{"version":3,"file":"inspect-ui.d.ts","sourceRoot":"","sources":["../../src/evolve/inspect-ui.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { Theme } from \"@ch1nyzzz/pi-coding-agent\";\nimport { type Component, Key, matchesKey, type TUI, wrapTextWithAnsi } from \"@ch1nyzzz/pi-tui\";\nimport { buildTrialComparison, type TrialComparison } from \"../comparison.ts\";\nimport type { EvoPaths } from \"../paths.ts\";\nimport { loadProposal, proposalApproval } from \"../proposal.ts\";\nimport { readEvaluationArtifact } from \"../proposal-artifacts.ts\";\nimport { recordPermitDefer, recordPermitReopen } from \"../reflect/permit.ts\";\nimport type { EvoService } from \"../service.ts\";\nimport type { ComponentApprovalDecision, EvolutionRun, EvolutionRunStatus, Proposal, TrialState } from \"../types.ts\";\nimport {\n\tactivityGroup,\n\ttype EvoActivityGroup,\n\ttype EvoActivityItem,\n\tgroupActivityItems,\n\tlistEvoActivityItems,\n} from \"./activity.ts\";\nimport { evolutionRunDirectory, readEvolutionRun } from \"./run.ts\";\nimport { type PendingVerification, readPendingVerification } from \"./verification-decision.ts\";\n\nexport type VerificationDecision = \"execute\" | \"skip\" | \"reject\";\n\ninterface TranscriptEvent {\n\ttimestamp: string;\n\tphase: string;\n\ttype: \"text\" | \"thinking\" | \"tool-arguments\" | \"tool-call\" | \"tool-result\" | \"complete\";\n\tdelta?: string;\n\tname?: string;\n\targuments?: Record<string, unknown>;\n\ttext?: string;\n\tisError?: boolean;\n\tstopReason?: string;\n}\n\ninterface RunArtifacts {\n\tplan?: string;\n\texperiment?: string;\n\tvalidation?: string;\n\tevaluation?: string;\n}\n\nconst PHASE_ORDER: EvolutionRunStatus[] = [\n\t\"queued\",\n\t\"researching\",\n\t\"planned\",\n\t\"building\",\n\t\"validating\",\n\t\"replaying\",\n\t\"evaluating\",\n\t\"awaiting-canary-approval\",\n\t\"trialing\",\n\t\"awaiting-decision\",\n\t\"completed\",\n];\n\nconst GROUP_LABELS: Record<EvoActivityGroup, string> = {\n\taction: \"需要你的决定\",\n\trunning: \"进行中\",\n\thistory: \"历史\",\n};\n\nconst PROPOSAL_ACTION_HOTKEYS: Record<ProposalActionId, string> = {\n\tapprove: \"a\",\n\treject: \"r\",\n\tdefer: \"d\",\n\treopen: \"o\",\n};\n\ninterface InspectorSection {\n\tid: string;\n\ttitle: string;\n\tsummary?: string;\n\ttone?: \"warning\";\n\tcontent: string[];\n}\n\n/** Collapse a paragraph to a single scannable line. */\nfunction summarize(value: string, maxLength = 96): string {\n\tconst normalized = value.replace(/\\s+/g, \" \").trim();\n\treturn normalized.length <= maxLength ? normalized : `${normalized.slice(0, maxLength - 1)}…`;\n}\n\ntype ProposalActionId = \"approve\" | \"reject\" | \"defer\" | \"reopen\";\n\nasync function readOptional(path: string, maxCharacters = 40_000): Promise<string | undefined> {\n\ttry {\n\t\tconst text = await readFile(path, \"utf8\");\n\t\treturn text.length > maxCharacters ? `${text.slice(0, maxCharacters)}\\n[内容已截断]` : text;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nasync function readTranscript(paths: EvoPaths, runId: string): Promise<TranscriptEvent[]> {\n\tconst text = await readOptional(join(evolutionRunDirectory(paths, runId), \"transcript.jsonl\"), 500_000);\n\tif (!text) return [];\n\treturn text.split(\"\\n\").flatMap((line) => {\n\t\ttry {\n\t\t\treturn [JSON.parse(line) as TranscriptEvent];\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t});\n}\n\nfunction cleanThinking(value: string): string {\n\treturn value.replaceAll(\"**\", \"\").replace(/\\s+/g, \" \").trim();\n}\n\nfunction elapsed(startedAt: string): string {\n\tconst seconds = Math.max(0, Math.floor((Date.now() - Date.parse(startedAt)) / 1_000));\n\tif (seconds < 60) return `${seconds}s`;\n\tconst minutes = Math.floor(seconds / 60);\n\treturn minutes < 60 ? `${minutes}m ${seconds % 60}s` : `${Math.floor(minutes / 60)}h ${minutes % 60}m`;\n}\n\nfunction phaseLabel(status: EvolutionRunStatus): string {\n\treturn {\n\t\tqueued: \"等待执行\",\n\t\tresearching: \"研究问题并制定实验计划\",\n\t\tplanned: \"研究计划已冻结\",\n\t\tbuilding: \"构建候选改进\",\n\t\tvalidating: \"执行确定性验证\",\n\t\treplaying: \"回放历史任务\",\n\t\tevaluating: \"评估候选效果\",\n\t\t\"awaiting-evidence\": \"等待更多实验数据\",\n\t\t\"awaiting-canary-approval\": \"等待用户确认 Canary\",\n\t\ttrialing: \"Canary 试运行中\",\n\t\t\"awaiting-decision\": \"等待最终决定\",\n\t\tpaused: \"已暂停\",\n\t\tcompleted: \"已完成\",\n\t\tfailed: \"执行失败\",\n\t\tcancelled: \"已取消\",\n\t}[status];\n}\n\nfunction toolDescription(event: TranscriptEvent): string {\n\tconst args = event.arguments ?? {};\n\tif (event.name === \"evo_research_search\") {\n\t\treturn `检索 ${String(args.source ?? \"资料源\")}：${String(args.query ?? \"\")}`;\n\t}\n\tif (event.name === \"evo_research_fetch\") return `读取研究资料：${String(args.url ?? args.id ?? \"\")}`;\n\treturn `${event.name ?? \"工具\"} ${Object.keys(args).length > 0 ? JSON.stringify(args) : \"\"}`.trim();\n}\n\nfunction currentActivity(events: TranscriptEvent[], status: EvolutionRunStatus): string {\n\tfor (let index = events.length - 1; index >= 0; index--) {\n\t\tconst event = events[index];\n\t\tif (!event) continue;\n\t\tif (event.type === \"text\" && cleanThinking(event.delta ?? \"\")) {\n\t\t\treturn `正在整理输出：${cleanThinking(event.delta ?? \"\")}`;\n\t\t}\n\t\tif (event.type === \"thinking\" && cleanThinking(event.delta ?? \"\")) return cleanThinking(event.delta ?? \"\");\n\t\tif (event.type === \"tool-call\") return toolDescription(event);\n\t\tif (event.type === \"tool-result\")\n\t\t\treturn event.isError ? `${event.name} 执行失败` : `${event.name} 已返回，正在分析`;\n\t}\n\treturn phaseLabel(status);\n}\n\nfunction stageProgress(theme: Theme, status: EvolutionRunStatus): string {\n\tconst current = status === \"awaiting-evidence\" ? 7 : PHASE_ORDER.indexOf(status);\n\tconst stages = [\n\t\t{ threshold: 1, label: \"研究\" },\n\t\t{ threshold: 3, label: \"构建\" },\n\t\t{ threshold: 4, label: \"验证\" },\n\t\t{ threshold: 5, label: \"回放\" },\n\t\t{ threshold: 6, label: \"评估\" },\n\t\t{ threshold: 8, label: \"Canary\" },\n\t\t{ threshold: 10, label: \"完成\" },\n\t];\n\treturn stages\n\t\t.map((stage) => {\n\t\t\tif (status === \"failed\" || status === \"cancelled\") return theme.fg(\"muted\", `○ ${stage.label}`);\n\t\t\tif (status === \"completed\" || current > stage.threshold) return theme.fg(\"success\", `● ${stage.label}`);\n\t\t\tif (current === stage.threshold || (stage.threshold === 1 && status === \"planned\")) {\n\t\t\t\treturn theme.fg(\"accent\", `◆ ${stage.label}`);\n\t\t\t}\n\t\t\treturn theme.fg(\"dim\", `○ ${stage.label}`);\n\t\t})\n\t\t.join(\"  \");\n}\n\nfunction thinkingText(events: TranscriptEvent[]): string {\n\treturn events\n\t\t.filter((event) => event.type === \"thinking\")\n\t\t.map((event) => event.delta ?? \"\")\n\t\t.join(\"\")\n\t\t.trim();\n}\n\nfunction toolEvents(events: TranscriptEvent[]): Array<{ call: TranscriptEvent; result?: TranscriptEvent }> {\n\tconst calls: Array<{ call: TranscriptEvent; result?: TranscriptEvent }> = [];\n\tfor (const event of events) {\n\t\tif (event.type === \"tool-call\") calls.push({ call: event });\n\t\telse if (event.type === \"tool-result\") {\n\t\t\tconst pending = [...calls].reverse().find((entry) => !entry.result && entry.call.name === event.name);\n\t\t\tif (pending) pending.result = event;\n\t\t} else if (event.type === \"thinking\" || event.type === \"text\") {\n\t\t\tfor (const pending of calls.filter((entry) => !entry.result)) {\n\t\t\t\tpending.result = {\n\t\t\t\t\ttimestamp: event.timestamp,\n\t\t\t\t\tphase: event.phase,\n\t\t\t\t\ttype: \"tool-result\",\n\t\t\t\t\tname: pending.call.name,\n\t\t\t\t\tisError: false,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t}\n\treturn calls;\n}\n\nexport class EvolutionProcessInspector implements Component {\n\tprivate timer: NodeJS.Timeout;\n\tprivate items: EvoActivityItem[] = [];\n\tprivate item?: EvoActivityItem;\n\tprivate run?: EvolutionRun;\n\tprivate events: TranscriptEvent[] = [];\n\tprivate artifacts: RunArtifacts = {};\n\tprivate proposal?: Proposal;\n\tprivate mode: \"tasks\" | \"run\";\n\tprivate taskIndex = 0;\n\tprivate displayItems: EvoActivityItem[] = [];\n\tprivate sectionFocus = 0;\n\tprivate expandedSections = new Set<string>();\n\tprivate activeSectionIds: string[] = [];\n\tprivate focusedSectionLine = -1;\n\tprivate followFocus = false;\n\tprivate scrollFromBottom = 0;\n\tprivate readonly tui: TUI;\n\tprivate readonly theme: Theme;\n\tprivate readonly paths: EvoPaths;\n\tprivate readonly service: EvoService;\n\tprivate readonly close: () => void;\n\tprivate readonly approveCanary: (runId: string, decision: ComponentApprovalDecision) => Promise<void>;\n\tprivate readonly directKeepCanary: (runId: string) => Promise<void>;\n\tprivate readonly decideVerification: (runId: string, decision: VerificationDecision) => Promise<void>;\n\tprivate pendingVerification?: PendingVerification;\n\tprivate verificationChoice = 0;\n\tprivate verificationActing = false;\n\tprivate verificationError?: string;\n\tprivate approving = false;\n\tprivate approvalError?: string;\n\tprivate approvalView: \"choice\" | \"custom\" | \"direct-confirm\" = \"choice\";\n\tprivate approvalChoice = 0;\n\tprivate customField = 0;\n\tprivate customMinimumSamples = 10;\n\tprivate customMaximumDurationDays = 7;\n\tprivate customDefaultsInitialized = false;\n\tprivate trial?: TrialState;\n\tprivate trialComparison?: TrialComparison;\n\tprivate nextTrialComparisonRefresh = 0;\n\tprivate keepConfirming = false;\n\tprivate keeping = false;\n\tprivate keepError?: string;\n\tprivate itemKey?: string;\n\tprivate loadError?: string;\n\tprivate taskNotice?: string;\n\tprivate taskScrollTop = 0;\n\tprivate selectedTaskLine = 0;\n\tprivate pendingInitialScroll: boolean;\n\tprivate proposalView: \"info\" | \"confirm\" | \"digest\" | \"reason\" = \"info\";\n\tprivate proposalAction: ProposalActionId = \"approve\";\n\tprivate proposalChoice = 0;\n\tprivate proposalInput = \"\";\n\tprivate proposalError?: string;\n\tprivate proposalActing = false;\n\tprivate proposalReview?: string;\n\tprivate proposalValidation?: string;\n\tprivate proposalArtifactsFor?: string;\n\n\tconstructor(\n\t\ttui: TUI,\n\t\ttheme: Theme,\n\t\tpaths: EvoPaths,\n\t\tservice: EvoService,\n\t\titemKey: string | undefined,\n\t\tclose: () => void,\n\t\tapproveCanary: (runId: string, decision: ComponentApprovalDecision) => Promise<void>,\n\t\tdirectKeepCanary: (runId: string) => Promise<void> = async () => {\n\t\t\tthrow new Error(\"Direct keep is unavailable\");\n\t\t},\n\t\tdecideVerification: (runId: string, decision: VerificationDecision) => Promise<void> = async () => {\n\t\t\tthrow new Error(\"Verification decisions are unavailable\");\n\t\t},\n\t) {\n\t\tthis.tui = tui;\n\t\tthis.theme = theme;\n\t\tthis.paths = paths;\n\t\tthis.service = service;\n\t\tthis.itemKey = itemKey;\n\t\tthis.close = close;\n\t\tthis.approveCanary = approveCanary;\n\t\tthis.directKeepCanary = directKeepCanary;\n\t\tthis.decideVerification = decideVerification;\n\t\tthis.mode = itemKey ? \"run\" : \"tasks\";\n\t\tthis.pendingInitialScroll = Boolean(itemKey);\n\t\tvoid this.refresh();\n\t\tthis.timer = setInterval(() => void this.refresh(), 250);\n\t\tthis.timer.unref?.();\n\t}\n\n\tprivate async refresh(): Promise<void> {\n\t\ttry {\n\t\t\tthis.items = await listEvoActivityItems(\n\t\t\t\t{ paths: this.paths, service: this.service },\n\t\t\t\t{ includeHistory: true, includeTrialComparison: false },\n\t\t\t);\n\t\t\tthis.displayItems = groupActivityItems(this.items);\n\t\t\tthis.taskIndex = Math.min(this.taskIndex, Math.max(0, this.displayItems.length - 1));\n\t\t\tif (this.mode === \"run\" && this.itemKey) {\n\t\t\t\tthis.item = this.items.find((item) => item.key === this.itemKey);\n\t\t\t\tif (!this.item) {\n\t\t\t\t\t// The referenced item no longer exists (processed elsewhere or a\n\t\t\t\t\t// stale status entry): fall back to the live task list instead of\n\t\t\t\t\t// polling a permanent \"connecting\" screen.\n\t\t\t\t\tthis.mode = \"tasks\";\n\t\t\t\t\tthis.itemKey = undefined;\n\t\t\t\t\tthis.run = undefined;\n\t\t\t\t\tthis.proposal = undefined;\n\t\t\t\t\tthis.taskNotice = \"所选事项已不存在（可能已被处理）；请从下面的列表重新选择\";\n\t\t\t\t\tthis.tui.requestRender();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (this.item.kind === \"proposal\") {\n\t\t\t\t\tthis.run = undefined;\n\t\t\t\t\tthis.proposal = this.item.proposal;\n\t\t\t\t\tthis.events = [];\n\t\t\t\t\tthis.artifacts = {};\n\t\t\t\t\tthis.trial = undefined;\n\t\t\t\t\tthis.trialComparison = undefined;\n\t\t\t\t\tconst artifactsKey = `${this.proposal.id}:r${this.proposal.revision}`;\n\t\t\t\t\tif (this.proposalArtifactsFor !== artifactsKey) {\n\t\t\t\t\t\tthis.proposalArtifactsFor = artifactsKey;\n\t\t\t\t\t\tthis.proposalReview = await this.readProposalArtifact(this.proposal, \"review\");\n\t\t\t\t\t\tthis.proposalValidation = await this.readProposalArtifact(this.proposal, \"validation\");\n\t\t\t\t\t}\n\t\t\t\t\tif (this.pendingInitialScroll) {\n\t\t\t\t\t\t// A proposal card is a static document: open it at the top.\n\t\t\t\t\t\tthis.scrollFromBottom = Number.MAX_SAFE_INTEGER;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tthis.run = await readEvolutionRun(this.paths, this.item.run.id);\n\t\t\t\t\tconst directory = evolutionRunDirectory(this.paths, this.run.id);\n\t\t\t\t\t[\n\t\t\t\t\t\tthis.events,\n\t\t\t\t\t\tthis.artifacts.plan,\n\t\t\t\t\t\tthis.artifacts.experiment,\n\t\t\t\t\t\tthis.artifacts.validation,\n\t\t\t\t\t\tthis.artifacts.evaluation,\n\t\t\t\t\t\tthis.proposal,\n\t\t\t\t\t] = await Promise.all([\n\t\t\t\t\t\treadTranscript(this.paths, this.run.id),\n\t\t\t\t\t\treadOptional(join(directory, \"plan.md\")),\n\t\t\t\t\t\treadOptional(join(directory, \"experiment.json\")),\n\t\t\t\t\t\treadOptional(join(directory, \"validation.md\")),\n\t\t\t\t\t\treadOptional(join(directory, \"evaluation.md\")),\n\t\t\t\t\t\tthis.run.proposalId ? loadProposal(this.paths, this.run.proposalId) : Promise.resolve(undefined),\n\t\t\t\t\t]);\n\t\t\t\t\tif (!this.artifacts.evaluation && this.proposal?.artifacts.review) {\n\t\t\t\t\t\tthis.artifacts.evaluation = await readOptional(\n\t\t\t\t\t\t\tjoin(this.paths.proposals, this.proposal.id, this.proposal.artifacts.review.file),\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tthis.pendingVerification =\n\t\t\t\t\t\tthis.run.status === \"awaiting-evidence\"\n\t\t\t\t\t\t\t? await readPendingVerification(this.paths, this.run.id)\n\t\t\t\t\t\t\t: undefined;\n\t\t\t\t\tconst status = await this.service.status();\n\t\t\t\t\tthis.trial = status.trial?.proposalId === this.proposal?.id ? status.trial : undefined;\n\t\t\t\t\tif (this.trial && this.proposal && Date.now() >= this.nextTrialComparisonRefresh) {\n\t\t\t\t\t\tthis.trialComparison = await buildTrialComparison(this.paths, this.proposal, this.trial);\n\t\t\t\t\t\tthis.nextTrialComparisonRefresh = Date.now() + 5_000;\n\t\t\t\t\t} else if (!this.trial) this.trialComparison = undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.pendingInitialScroll = false;\n\t\t\tthis.loadError = undefined;\n\t\t\tthis.tui.requestRender();\n\t\t} catch (error) {\n\t\t\tthis.loadError = error instanceof Error ? error.message : String(error);\n\t\t\tthis.tui.requestRender();\n\t\t}\n\t}\n\n\tprivate itemGlyph(item: EvoActivityItem): {\n\t\ticon: string;\n\t\tcolor: \"warning\" | \"muted\" | \"accent\" | \"success\" | \"error\";\n\t} {\n\t\tif (item.kind === \"proposal\") {\n\t\t\tif (item.proposal.status === \"pending\") return { icon: \"●\", color: \"warning\" };\n\t\t\tif (item.proposal.status === \"deferred\") return { icon: \"○\", color: \"muted\" };\n\t\t\treturn { icon: \"◆\", color: \"accent\" };\n\t\t}\n\t\tconst status = item.run.status;\n\t\tif (status === \"trialing\") return { icon: \"◆\", color: \"accent\" };\n\t\tif (status === \"awaiting-canary-approval\" || status === \"awaiting-decision\")\n\t\t\treturn { icon: \"●\", color: \"warning\" };\n\t\tif (status === \"completed\") return { icon: \"✓\", color: \"success\" };\n\t\tif (status === \"failed\" || status === \"cancelled\") return { icon: \"✗\", color: \"error\" };\n\t\tif (status === \"paused\") return { icon: \"○\", color: \"warning\" };\n\t\treturn { icon: \"▶\", color: \"success\" };\n\t}\n\n\tprivate taskLines(): string[] {\n\t\tconst actionCount = this.displayItems.filter((item) => activityGroup(item) === \"action\").length;\n\t\tconst lines = [\n\t\t\tthis.theme.fg(\"accent\", this.theme.bold(\"Evo 工作流\")) +\n\t\t\t\t(actionCount > 0\n\t\t\t\t\t? this.theme.fg(\"warning\", this.theme.bold(` · ${actionCount} 项等待你处理`))\n\t\t\t\t\t: this.theme.fg(\"dim\", ` · ${this.displayItems.length} 项`)),\n\t\t\t...(this.taskNotice ? [\"\", this.theme.fg(\"warning\", this.taskNotice)] : []),\n\t\t\t\"\",\n\t\t];\n\t\tif (this.displayItems.length === 0) {\n\t\t\tlines.push(\n\t\t\t\tthis.theme.fg(\"muted\", \"暂无事项\"),\n\t\t\t\tthis.theme.fg(\"dim\", \"/evo go <目标> 启动一次演化 · /evo help 查看全部命令\"),\n\t\t\t);\n\t\t}\n\t\tthis.selectedTaskLine = 0;\n\t\tlet lastGroup: EvoActivityGroup | undefined;\n\t\tfor (const [index, item] of this.displayItems.entries()) {\n\t\t\tconst group = activityGroup(item);\n\t\t\tif (group !== lastGroup) {\n\t\t\t\tif (lastGroup !== undefined) lines.push(\"\");\n\t\t\t\tconst headerColor = group === \"action\" ? \"warning\" : group === \"running\" ? \"text\" : \"dim\";\n\t\t\t\tlines.push(this.theme.fg(headerColor, this.theme.bold(GROUP_LABELS[group])));\n\t\t\t\tlastGroup = group;\n\t\t\t}\n\t\t\tconst selected = index === this.taskIndex;\n\t\t\tif (selected) this.selectedTaskLine = lines.length;\n\t\t\tconst marker = selected ? this.theme.fg(\"accent\", \"›\") : \" \";\n\t\t\tconst glyph = this.itemGlyph(item);\n\t\t\tconst text = item.text.replace(/^Evo: /, \"\");\n\t\t\tconst textColor = group === \"history\" ? \"muted\" : \"text\";\n\t\t\tconst suffix = item.kind === \"run\" ? this.theme.fg(\"dim\", `  ${elapsed(item.run.startedAt)}`) : \"\";\n\t\t\tlines.push(`${marker} ${this.theme.fg(glyph.color, glyph.icon)} ${this.theme.fg(textColor, text)}${suffix}`);\n\t\t}\n\t\treturn lines;\n\t}\n\n\tprivate pushSections(lines: string[], sections: InspectorSection[]): void {\n\t\tthis.activeSectionIds = sections.map((section) => section.id);\n\t\tthis.sectionFocus = Math.min(this.sectionFocus, Math.max(0, sections.length - 1));\n\t\tfor (const [index, section] of sections.entries()) {\n\t\t\tif (index === this.sectionFocus) this.focusedSectionLine = lines.length;\n\t\t\tconst focused = index === this.sectionFocus;\n\t\t\tconst expanded = this.expandedSections.has(section.id);\n\t\t\tconst marker = focused ? this.theme.fg(\"accent\", \"›\") : \" \";\n\t\t\tconst triangle = expanded ? \"▾\" : \"▸\";\n\t\t\tconst title =\n\t\t\t\tsection.tone === \"warning\"\n\t\t\t\t\t? this.theme.fg(\"warning\", this.theme.bold(section.title))\n\t\t\t\t\t: this.theme.bold(section.title);\n\t\t\tlines.push(\n\t\t\t\t`${marker} ${triangle} ${title}${section.summary ? `  ${this.theme.fg(\"muted\", section.summary)}` : \"\"}`,\n\t\t\t);\n\t\t\tif (expanded) lines.push(...section.content.map((line) => `  ${line}`), \"\");\n\t\t}\n\t}\n\n\tprivate focusNextSection(): void {\n\t\tif (this.activeSectionIds.length === 0) return;\n\t\tthis.sectionFocus = (this.sectionFocus + 1) % this.activeSectionIds.length;\n\t\tthis.followFocus = true;\n\t}\n\n\tprivate moveSectionFocus(delta: number): void {\n\t\tif (this.activeSectionIds.length === 0) return;\n\t\tthis.sectionFocus = Math.min(Math.max(0, this.sectionFocus + delta), this.activeSectionIds.length - 1);\n\t\tthis.followFocus = true;\n\t}\n\n\tprivate toggleFocusedSection(): void {\n\t\tconst id = this.activeSectionIds[this.sectionFocus];\n\t\tif (!id) return;\n\t\tif (this.expandedSections.has(id)) this.expandedSections.delete(id);\n\t\telse this.expandedSections.add(id);\n\t\tthis.followFocus = true;\n\t}\n\n\tprivate tierBadge(proposal: Proposal): string {\n\t\tconst color = proposal.tier === \"T0\" ? \"success\" : proposal.tier === \"T1\" ? \"warning\" : \"error\";\n\t\treturn this.theme.fg(color, this.theme.bold(`[${proposal.tier}/${proposal.kind}]`));\n\t}\n\n\tprivate proposalStatusMeta(proposal: Proposal): {\n\t\tlabel: string;\n\t\tcolor: \"warning\" | \"muted\" | \"accent\" | \"success\" | \"error\";\n\t} {\n\t\tswitch (proposal.status) {\n\t\t\tcase \"pending\":\n\t\t\t\treturn { label: \"等待处理\", color: \"warning\" };\n\t\t\tcase \"deferred\":\n\t\t\t\treturn { label: \"已推迟\", color: \"muted\" };\n\t\t\tcase \"approved\":\n\t\t\t\treturn { label: \"已批准\", color: \"success\" };\n\t\t\tcase \"trialing\":\n\t\t\t\treturn { label: \"Trial 运行中\", color: \"accent\" };\n\t\t\tcase \"kept\":\n\t\t\t\treturn { label: \"已保留\", color: \"success\" };\n\t\t\tcase \"rejected\":\n\t\t\t\treturn { label: \"已拒绝\", color: \"error\" };\n\t\t\tcase \"rolled-back\":\n\t\t\t\treturn { label: \"已回滚\", color: \"muted\" };\n\t\t\tdefault:\n\t\t\t\treturn { label: proposal.status, color: \"muted\" };\n\t\t}\n\t}\n\n\tprivate progressBar(current: number, minimum: number): string {\n\t\tconst width = 10;\n\t\tconst filled = Math.min(width, Math.round((width * current) / Math.max(1, minimum)));\n\t\tconst color = current >= minimum ? \"success\" : \"accent\";\n\t\treturn `${this.theme.fg(color, \"●\".repeat(filled))}${this.theme.fg(\"dim\", \"○\".repeat(width - filled))}`;\n\t}\n\n\tprivate initializeCustomCanaryDefaults(): void {\n\t\tif (this.customDefaultsInitialized) return;\n\t\ttry {\n\t\t\tconst experiment = JSON.parse(this.artifacts.experiment ?? \"null\") as {\n\t\t\t\tevidenceStrategy?: { online?: { mode?: string; minimumSamples?: number; maximumDuration?: string } };\n\t\t\t};\n\t\t\tconst online = experiment.evidenceStrategy?.online;\n\t\t\tif (online?.mode === \"canary\") {\n\t\t\t\tif (Number.isSafeInteger(online.minimumSamples) && (online.minimumSamples ?? 0) > 0) {\n\t\t\t\t\tthis.customMinimumSamples = online.minimumSamples ?? this.customMinimumSamples;\n\t\t\t\t}\n\t\t\t\tconst days = /^([1-9]\\d*)d$/.exec(online.maximumDuration ?? \"\");\n\t\t\t\tif (days) this.customMaximumDurationDays = Number(days[1]);\n\t\t\t}\n\t\t} catch {\n\t\t\t// The exact experiment remains visible; malformed display defaults do not enable approval.\n\t\t}\n\t\tthis.customDefaultsInitialized = true;\n\t}\n\n\tprivate approvalControlLines(metadataMatches: boolean, directAvailable: boolean): string[] {\n\t\tif (!metadataMatches) return [this.theme.fg(\"error\", \"审批已禁用：运行记录与当前提案不一致\")];\n\t\tif (this.approving) return [this.theme.fg(\"accent\", \"正在应用人工发布决策……\")];\n\t\tif (this.approvalView === \"direct-confirm\") {\n\t\t\treturn [\n\t\t\t\tthis.theme.fg(\"warning\", this.theme.bold(\"直接上线将跳过所有 Canary 效果证据。\")),\n\t\t\t\t\"候选会立即成为 stable；其 parent 仍可通过 /evo rollback 恢复。\",\n\t\t\t\tthis.theme.fg(\"accent\", \"再次按 Enter 确认直接上线 · Esc 返回选择\"),\n\t\t\t];\n\t\t}\n\t\tif (this.approvalView === \"custom\") {\n\t\t\tconst rows = [\n\t\t\t\t`最少 candidate sessions：${this.customMinimumSamples}`,\n\t\t\t\t`最长持续时间：${this.customMaximumDurationDays} 天`,\n\t\t\t\t\"以以上参数启动 Canary\",\n\t\t\t];\n\t\t\treturn [\n\t\t\t\tthis.theme.bold(\"自定义 Canary\"),\n\t\t\t\t...rows.map((row, index) => `${index === this.customField ? this.theme.fg(\"accent\", \"›\") : \" \"} ${row}`),\n\t\t\t\tthis.theme.fg(\"dim\", \"↑↓ 选择 · ←→ 调整 · Enter 启动 · Esc 返回\"),\n\t\t\t];\n\t\t}\n\t\tconst choices = [\n\t\t\t{ key: \"c\", label: \"默认 Canary\", note: \"按冻结计划与本地默认值\" },\n\t\t\t{ key: \"u\", label: \"自定义 Canary\", note: \"样本数与最长时间\" },\n\t\t\tdirectAvailable\n\t\t\t\t? { key: \"x\", label: \"直接上线\", note: \"跳过 Canary 效果证据\" }\n\t\t\t\t: { key: \"x\", label: \"直接上线不可用\", note: \"缺少可执行 validation artifact\" },\n\t\t];\n\t\treturn [\n\t\t\tthis.theme.bold(\"发布决策\"),\n\t\t\t...choices.map((choice, index) => {\n\t\t\t\tconst marker = index === this.approvalChoice ? this.theme.fg(\"accent\", \"›\") : \" \";\n\t\t\t\treturn `${marker} ${this.theme.fg(\"accent\", `[${choice.key}]`)} ${choice.label}  ${this.theme.fg(\"muted\", choice.note)}`;\n\t\t\t}),\n\t\t\tthis.theme.fg(\n\t\t\t\t\"dim\",\n\t\t\t\t\"推荐默认 Canary：先收集效果证据，可随时回滚 · 快捷键或 ←→+Enter · Tab 章节 · ↑↓ 滚动 · Esc 返回\",\n\t\t\t),\n\t\t];\n\t}\n\n\tprivate canaryLines(run: EvolutionRun): string[] {\n\t\tconst proposal = this.proposal;\n\t\tconst validation = this.artifacts.validation?.trim();\n\t\tconst evaluation = this.artifacts.evaluation?.trim();\n\t\tconst diff = proposal?.diff.trim() ?? \"\";\n\t\tconst sections: InspectorSection[] = [\n\t\t\t{\n\t\t\t\tid: \"risk\",\n\t\t\t\ttitle: \"风险\",\n\t\t\t\ttone: \"warning\",\n\t\t\t\tsummary: summarize(proposal?.risk ?? \"unknown\", 48),\n\t\t\t\tcontent: [proposal?.risk ?? \"unknown\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"rollback\",\n\t\t\t\ttitle: \"Canary 与回滚计划\",\n\t\t\t\tsummary: summarize(proposal?.trialPlan ?? \"unknown\", 48),\n\t\t\t\tcontent: [proposal?.trialPlan ?? \"unknown\", \"\", \"回滚将恢复 Canary 启动前的 stable bundle。\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"validation\",\n\t\t\t\ttitle: \"可执行验证\",\n\t\t\t\tsummary: validation ? \"✓ 已完成\" : \"缺失\",\n\t\t\t\tcontent: validation ? validation.split(\"\\n\") : [\"没有可执行验证报告\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"evaluation\",\n\t\t\t\ttitle: \"独立评估\",\n\t\t\t\tsummary: evaluation ? \"✓ 已完成\" : \"缺失\",\n\t\t\t\tcontent: evaluation ? evaluation.split(\"\\n\") : [\"没有评估报告\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"diff\",\n\t\t\t\ttitle: `Exact diff${diff ? ` · ${diff.split(\"\\n\").length} 行` : \"\"}`,\n\t\t\t\tcontent: diff ? diff.split(\"\\n\") : [\"没有可审阅 diff\"],\n\t\t\t},\n\t\t];\n\t\tconst facts = [\n\t\t\t`验证 ${validation ? \"✓\" : \"–\"}`,\n\t\t\t`评估 ${evaluation ? \"✓\" : \"–\"}`,\n\t\t\t...(proposal ? [`diff ${proposal.diffDigest.slice(0, 12)}…`] : []),\n\t\t].join(\" · \");\n\t\tconst lines = [\n\t\t\tthis.theme.fg(\"accent\", this.theme.bold(\"Evo Component Canary 审批\")),\n\t\t\t\"\",\n\t\t\t`${this.theme.bold(\"Component\")}  ${this.item?.component ?? \"unknown\"}${this.theme.fg(\n\t\t\t\t\"dim\",\n\t\t\t\t` · ${run.canaryTargetAbi ?? \"unknown ABI\"} · r${proposal?.revision ?? \"?\"}`,\n\t\t\t)}`,\n\t\t\t`${this.theme.bold(\"目标\")}  ${run.request ?? \"定时演化\"}`,\n\t\t\t`${this.theme.fg(\"dim\", \"Changed paths\")}  ${proposal?.changedPaths.join(\", \") || \"unknown\"}`,\n\t\t\t\"\",\n\t\t\t`${this.theme.fg(\"warning\", \"风险\")}  ${this.theme.fg(\"warning\", summarize(proposal?.risk ?? \"unknown\"))}`,\n\t\t\tthis.theme.fg(\"dim\", facts),\n\t\t\t\"\",\n\t\t];\n\t\tthis.pushSections(lines, sections);\n\t\treturn lines;\n\t}\n\n\tprivate canaryDecisionLines(): string[] {\n\t\tconst run = this.run;\n\t\tif (!run) return [];\n\t\tthis.initializeCustomCanaryDefaults();\n\t\tconst proposal = this.proposal;\n\t\tconst metadataMatches =\n\t\t\tproposal !== undefined &&\n\t\t\tproposal.status === \"pending\" &&\n\t\t\tproposal.parentBundleDigest === run.canaryParentDigest &&\n\t\t\tproposal.candidateDigest === run.canaryCandidateDigest &&\n\t\t\tproposal.targetAbi === run.canaryTargetAbi;\n\t\treturn [\n\t\t\tthis.theme.fg(\"dim\", \"─\".repeat(40)),\n\t\t\t...(this.approvalError ? [this.theme.fg(\"error\", this.approvalError)] : []),\n\t\t\t...this.approvalControlLines(metadataMatches, proposal?.artifacts.validation !== undefined),\n\t\t];\n\t}\n\n\tprivate activeCanaryLines(): string[] {\n\t\tconst proposal = this.proposal;\n\t\tconst trial = this.trial;\n\t\tconst comparison = this.trialComparison;\n\t\tconst component = this.item?.component ?? \"unknown\";\n\t\tconst surface = proposal?.targetAbi?.split(\"/\", 1)[0] ?? \"component\";\n\t\tconst shortComponent = component.endsWith(`-${surface}`) ? component.slice(0, -surface.length - 1) : component;\n\t\tconst minimumSamples = trial?.canary?.minimumSamples;\n\t\tconst currentSamples = comparison?.after.totals.sessions;\n\t\tconst duration = trial?.canary?.maximumDurationDays;\n\t\tconst progress =\n\t\t\tminimumSamples !== undefined && currentSamples !== undefined\n\t\t\t\t? `${this.progressBar(currentSamples, minimumSamples)}  ${currentSamples}/${minimumSamples} sessions`\n\t\t\t\t: \"等待 session evidence\";\n\t\tconst validation = this.artifacts.validation?.trim();\n\t\tconst evaluation = this.artifacts.evaluation?.trim();\n\t\tconst sections: InspectorSection[] = [\n\t\t\t{\n\t\t\t\tid: \"risk\",\n\t\t\t\ttitle: \"风险\",\n\t\t\t\ttone: \"warning\",\n\t\t\t\tsummary: summarize(proposal?.risk ?? \"unknown\", 48),\n\t\t\t\tcontent: [proposal?.risk ?? \"unknown\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"rollback\",\n\t\t\t\ttitle: \"Canary 与回滚计划\",\n\t\t\t\tsummary: summarize(trial?.plan ?? proposal?.trialPlan ?? \"unknown\", 48),\n\t\t\t\tcontent: [trial?.plan ?? proposal?.trialPlan ?? \"unknown\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"validation\",\n\t\t\t\ttitle: \"可执行验证\",\n\t\t\t\tsummary: validation ? \"✓ 已完成\" : \"缺失\",\n\t\t\t\tcontent: validation ? validation.split(\"\\n\") : [\"没有可执行 validation artifact\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"evaluation\",\n\t\t\t\ttitle: \"独立评估\",\n\t\t\t\tsummary: evaluation ? \"✓ 已完成\" : \"缺失\",\n\t\t\t\tcontent: evaluation ? evaluation.split(\"\\n\") : [\"没有评估报告\"],\n\t\t\t},\n\t\t];\n\t\tconst lines = [\n\t\t\tthis.theme.fg(\"accent\", this.theme.bold(\"Evo Component Canary\")),\n\t\t\t\"\",\n\t\t\t`${this.theme.bold(\"Component\")}  ${surface}/${shortComponent}${this.theme.fg(\n\t\t\t\t\"dim\",\n\t\t\t\t` · ${trial?.canary?.customization === \"custom\" ? \"自定义 Canary\" : \"默认 Canary\"}`,\n\t\t\t)}`,\n\t\t\t`${this.theme.bold(\"进度\")}  ${progress}${duration === undefined ? \"\" : ` · ≤${duration} 天`}`,\n\t\t\t\"\",\n\t\t\t`${this.theme.fg(\"warning\", \"风险\")}  ${this.theme.fg(\"warning\", summarize(proposal?.risk ?? \"unknown\"))}`,\n\t\t\t\"\",\n\t\t];\n\t\tthis.pushSections(lines, sections);\n\t\treturn lines;\n\t}\n\n\tprivate verificationDecisionLines(): string[] {\n\t\tconst pending = this.pendingVerification;\n\t\tif (!pending) return [];\n\t\tconst verdictLabel =\n\t\t\tpending.verdict === \"verified\"\n\t\t\t\t? \"verified\"\n\t\t\t\t: pending.verdict === \"needs-evidence\"\n\t\t\t\t\t? \"needs-evidence\"\n\t\t\t\t\t: pending.verdict;\n\t\tconst choices = [\n\t\t\t{ key: \"e\", label: \"执行验证\", note: `补跑 ${pending.profiles.join(\", \")} 后重新评估` },\n\t\t\t{ key: \"s\", label: \"跳过并发布\", note: `按当前评估结果（${verdictLabel}）走标准发布策略` },\n\t\t\t{ key: \"r\", label: \"拒绝候选\", note: \"不再投入验证，直接拒绝\" },\n\t\t];\n\t\tconst header = [\n\t\t\tthis.theme.fg(\"dim\", \"─\".repeat(40)),\n\t\t\tthis.theme.bold(\"研究者建议执行验证（非发布阻塞）\"),\n\t\t\t`${this.theme.fg(\"warning\", \"建议理由\")}  ${summarize(pending.reason, 120)}`,\n\t\t\tthis.theme.fg(\n\t\t\t\t\"dim\",\n\t\t\t\t`profiles: ${pending.profiles.join(\", \")} · datasets: ${pending.datasets.length} 个 · 最少样本 ${pending.minimumSamples} · 当前评估 ${verdictLabel}`,\n\t\t\t),\n\t\t];\n\t\tif (this.verificationActing) return [...header, this.theme.fg(\"accent\", \"正在应用验证决策……\")];\n\t\treturn [\n\t\t\t...header,\n\t\t\t...(this.verificationError ? [this.theme.fg(\"error\", this.verificationError)] : []),\n\t\t\t...choices.map((choice, index) => {\n\t\t\t\tconst marker = index === this.verificationChoice ? this.theme.fg(\"accent\", \"›\") : \" \";\n\t\t\t\treturn `${marker} ${this.theme.fg(\"accent\", `[${choice.key}]`)} ${choice.label}  ${this.theme.fg(\"muted\", choice.note)}`;\n\t\t\t}),\n\t\t\tthis.theme.fg(\"dim\", \"快捷键或 ←→+Enter · Tab 章节 · ↑↓ 滚动 · Esc 返回\"),\n\t\t];\n\t}\n\n\tprivate startVerificationDecision(decision: VerificationDecision): void {\n\t\tif (!this.run || this.verificationActing) return;\n\t\tthis.verificationActing = true;\n\t\tthis.verificationError = undefined;\n\t\tvoid this.decideVerification(this.run.id, decision)\n\t\t\t.then(() => this.refresh())\n\t\t\t.catch((error) => {\n\t\t\t\tthis.verificationError = error instanceof Error ? error.message : String(error);\n\t\t\t})\n\t\t\t.finally(() => {\n\t\t\t\tthis.verificationActing = false;\n\t\t\t\tthis.tui.requestRender();\n\t\t\t});\n\t}\n\n\tprivate trialingDecisionLines(): string[] {\n\t\tconst decision = this.keepConfirming\n\t\t\t? [\n\t\t\t\t\tthis.theme.fg(\"warning\", this.theme.bold(\"立即正式上线将结束 Canary，不再等待剩余效果证据。\")),\n\t\t\t\t\t\"当前 candidate 保持为 stable；parent 仍可通过 /evo rollback 恢复。\",\n\t\t\t\t\tthis.theme.fg(\"accent\", \"再次按 Enter 确认正式上线 · Esc 取消\"),\n\t\t\t\t]\n\t\t\t: [\n\t\t\t\t\t\"按 Enter 立即正式上线（随后需要二次确认）\",\n\t\t\t\t\tthis.theme.fg(\"dim\", \"Esc 返回事项列表 · /evo rollback 可立即回滚 · Tab 章节 · ↑↓ 滚动\"),\n\t\t\t\t];\n\t\treturn [\n\t\t\tthis.theme.fg(\"dim\", \"─\".repeat(40)),\n\t\t\t...(this.keepError ? [this.theme.fg(\"error\", this.keepError)] : []),\n\t\t\t...(this.keeping ? [this.theme.fg(\"accent\", \"正在正式上线……\")] : decision),\n\t\t];\n\t}\n\n\tprivate pinnedLines(): string[] {\n\t\tif (this.mode === \"tasks\") {\n\t\t\treturn [this.theme.fg(\"dim\", \"─\".repeat(40)), this.theme.fg(\"dim\", \"↑↓ 选择 · Enter 打开 · Esc 关闭\")];\n\t\t}\n\t\tif (this.item?.kind === \"proposal\") return this.proposalActionLines(this.item.proposal);\n\t\tif (this.run?.status === \"awaiting-canary-approval\") return this.canaryDecisionLines();\n\t\tif (this.run?.status === \"awaiting-evidence\" && this.pendingVerification) return this.verificationDecisionLines();\n\t\tif (this.run?.status === \"trialing\" && this.item?.component) return this.trialingDecisionLines();\n\t\tif (this.run) {\n\t\t\treturn [\n\t\t\t\tthis.theme.fg(\"dim\", \"─\".repeat(40)),\n\t\t\t\tthis.theme.fg(\"dim\", \"↑↓ 滚动 · Tab 选择章节 · Enter/Space 展开收起 · Esc 返回 · 实时更新\"),\n\t\t\t];\n\t\t}\n\t\treturn [];\n\t}\n\n\tprivate async readProposalArtifact(proposal: Proposal, kind: \"review\" | \"validation\"): Promise<string | undefined> {\n\t\tconst reference = proposal.artifacts[kind];\n\t\tif (!reference) return undefined;\n\t\ttry {\n\t\t\treturn (\n\t\t\t\tawait readEvaluationArtifact({\n\t\t\t\t\tpaths: this.paths,\n\t\t\t\t\tproposalId: proposal.id,\n\t\t\t\t\trevision: proposal.revision,\n\t\t\t\t\tdiffDigest: proposal.diffDigest,\n\t\t\t\t\tkind,\n\t\t\t\t\treference,\n\t\t\t\t})\n\t\t\t).trim();\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate proposalLines(proposal: Proposal): string[] {\n\t\tconst status = this.proposalStatusMeta(proposal);\n\t\tconst diffLines = proposal.diff.split(\"\\n\");\n\t\tconst maxDiffLines = 400;\n\t\tconst sections: InspectorSection[] = [\n\t\t\t{\n\t\t\t\tid: \"detail\",\n\t\t\t\ttitle: \"目标与预期效果\",\n\t\t\t\tsummary: summarize(proposal.motivation, 48),\n\t\t\t\tcontent: [\n\t\t\t\t\tthis.theme.bold(\"目标\"),\n\t\t\t\t\tproposal.motivation,\n\t\t\t\t\t\"\",\n\t\t\t\t\tthis.theme.bold(\"预期效果\"),\n\t\t\t\t\tproposal.expectedEffect,\n\t\t\t\t],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"risk\",\n\t\t\t\ttitle: \"风险\",\n\t\t\t\ttone: \"warning\",\n\t\t\t\tsummary: summarize(proposal.risk, 48),\n\t\t\t\tcontent: [proposal.risk],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"verify\",\n\t\t\t\ttitle: \"验证计划\",\n\t\t\t\tsummary: summarize(proposal.verifyPlan, 48),\n\t\t\t\tcontent: [proposal.verifyPlan],\n\t\t\t},\n\t\t];\n\t\tif (this.proposalValidation) {\n\t\t\tsections.push({\n\t\t\t\tid: \"validation\",\n\t\t\t\ttitle: \"可执行验证\",\n\t\t\t\tsummary: \"✓ 已完成\",\n\t\t\t\tcontent: this.proposalValidation.split(\"\\n\"),\n\t\t\t});\n\t\t}\n\t\tif (this.proposalReview) {\n\t\t\tsections.push({\n\t\t\t\tid: \"review\",\n\t\t\t\ttitle: \"独立评审\",\n\t\t\t\tsummary: \"✓ 已完成\",\n\t\t\t\tcontent: this.proposalReview.split(\"\\n\"),\n\t\t\t});\n\t\t}\n\t\tsections.push({\n\t\t\tid: \"diff\",\n\t\t\ttitle: `变更 · ${diffLines.length} 行`,\n\t\t\tcontent: [\n\t\t\t\t...diffLines.slice(0, maxDiffLines),\n\t\t\t\t...(diffLines.length > maxDiffLines\n\t\t\t\t\t? [this.theme.fg(\"dim\", `… 其余 ${diffLines.length - maxDiffLines} 行见 /evo show ${proposal.id}`)]\n\t\t\t\t\t: []),\n\t\t\t],\n\t\t});\n\t\tconst facts = [\n\t\t\t`验证 ${this.proposalValidation ? \"✓\" : \"–\"}`,\n\t\t\t`评审 ${this.proposalReview ? \"✓\" : \"–\"}`,\n\t\t\t`diff ${proposal.diffDigest.slice(0, 12)}…`,\n\t\t].join(\" · \");\n\t\tconst lines = [\n\t\t\tthis.theme.fg(\"accent\", this.theme.bold(\"Evo Proposal\")) +\n\t\t\t\t(this.item?.component ? this.theme.fg(\"dim\", ` · ${this.item.component}`) : \"\"),\n\t\t\t\"\",\n\t\t\t`${this.tierBadge(proposal)} ${this.theme.fg(status.color, status.label)}${this.theme.fg(\"dim\", ` · r${proposal.revision}`)}`,\n\t\t\t\"\",\n\t\t\t`${this.theme.fg(\"dim\", \"目标\")}  ${summarize(proposal.motivation)}`,\n\t\t\t`${this.theme.fg(\"dim\", \"预期\")}  ${summarize(proposal.expectedEffect)}`,\n\t\t\t`${this.theme.fg(\"warning\", \"风险\")}  ${this.theme.fg(\"warning\", summarize(proposal.risk))}`,\n\t\t\tthis.theme.fg(\"dim\", facts),\n\t\t\t\"\",\n\t\t];\n\t\tthis.pushSections(lines, sections);\n\t\treturn lines;\n\t}\n\n\tprivate availableProposalActions(proposal: Proposal): Array<{ id: ProposalActionId; label: string }> {\n\t\tif (proposal.status === \"pending\") {\n\t\t\treturn [\n\t\t\t\t{ id: \"approve\", label: \"批准\" },\n\t\t\t\t{ id: \"reject\", label: \"拒绝\" },\n\t\t\t\t{ id: \"defer\", label: \"推迟\" },\n\t\t\t];\n\t\t}\n\t\tif (proposal.status === \"deferred\") {\n\t\t\treturn [\n\t\t\t\t{ id: \"reopen\", label: \"重新打开\" },\n\t\t\t\t{ id: \"reject\", label: \"拒绝\" },\n\t\t\t];\n\t\t}\n\t\treturn [];\n\t}\n\n\tprivate strictProposalDigest(proposal: Proposal): string | undefined {\n\t\tif (proposal.kind !== \"code\" && proposal.tier !== \"T2\") return undefined;\n\t\treturn proposal.kind === \"code\" ? proposal.approvalDigest : proposal.diffDigest;\n\t}\n\n\tprivate proposalActionLines(proposal: Proposal): string[] {\n\t\tconst divider = this.theme.fg(\"dim\", \"─\".repeat(40));\n\t\tconst error = this.proposalError ? [this.theme.fg(\"error\", this.proposalError)] : [];\n\t\tif (this.proposalActing) return [divider, ...error, this.theme.fg(\"accent\", \"正在执行……\")];\n\t\tif (this.proposalView === \"reason\") {\n\t\t\tconst label = this.proposalAction === \"reject\" ? \"拒绝理由\" : \"推迟理由\";\n\t\t\treturn [\n\t\t\t\tdivider,\n\t\t\t\t...error,\n\t\t\t\t`${this.theme.bold(label)}：${this.proposalInput}${this.theme.fg(\"accent\", \"▌\")}`,\n\t\t\t\tthis.theme.fg(\"dim\", \"输入理由后 Enter 提交 • Esc 取消\"),\n\t\t\t];\n\t\t}\n\t\tif (this.proposalView === \"digest\") {\n\t\t\tconst expected = this.strictProposalDigest(proposal) ?? \"\";\n\t\t\treturn [\n\t\t\t\tdivider,\n\t\t\t\t...error,\n\t\t\t\tthis.theme.fg(\"warning\", \"严格审批：输入（可粘贴）完整 digest 以确认批准\"),\n\t\t\t\tthis.theme.fg(\"dim\", `需要输入：${expected}`),\n\t\t\t\t`输入：${this.proposalInput}${this.theme.fg(\"accent\", \"▌\")}`,\n\t\t\t\tthis.theme.fg(\"dim\", \"Enter 提交 • Esc 取消\"),\n\t\t\t];\n\t\t}\n\t\tif (this.proposalView === \"confirm\") {\n\t\t\treturn [\n\t\t\t\tdivider,\n\t\t\t\t...error,\n\t\t\t\tthis.theme.fg(\n\t\t\t\t\t\"warning\",\n\t\t\t\t\t`确认批准 ${proposal.tier}/${proposal.kind} 提案并应用 diff ${proposal.diffDigest.slice(0, 12)}…？`,\n\t\t\t\t),\n\t\t\t\tthis.theme.fg(\"accent\", \"再次按 Enter 确认 · Esc 取消\"),\n\t\t\t];\n\t\t}\n\t\tconst actions = this.availableProposalActions(proposal);\n\t\tif (actions.length === 0) {\n\t\t\treturn [\n\t\t\t\tdivider,\n\t\t\t\t...error,\n\t\t\t\tthis.theme.fg(\"dim\", \"此提案已处理完毕 · ↑↓ 章节 · Enter/Space 展开收起 · Esc 返回事项列表\"),\n\t\t\t];\n\t\t}\n\t\tconst row = actions\n\t\t\t.map((action, index) => {\n\t\t\t\tconst label = `[${PROPOSAL_ACTION_HOTKEYS[action.id]}] ${action.label}`;\n\t\t\t\treturn index === this.proposalChoice ? this.theme.fg(\"accent\", label) : this.theme.fg(\"muted\", label);\n\t\t\t})\n\t\t\t.join(\"  \");\n\t\tconst consequence = this.strictProposalDigest(proposal)\n\t\t\t? \"严格审批：批准需输入完整 digest\"\n\t\t\t: \"批准后按冻结策略生效或进入 Trial · 可随时 /evo rollback\";\n\t\treturn [\n\t\t\tdivider,\n\t\t\t...error,\n\t\t\t`${this.theme.bold(\"处理此提案\")}  ${row}`,\n\t\t\tthis.theme.fg(\"dim\", `${consequence} · 快捷键或 ←→+Enter · ↑↓ 章节 · Space 展开收起 · Esc 返回`),\n\t\t];\n\t}\n\n\tprivate startProposalAction(proposal: Proposal): void {\n\t\tconst actions = this.availableProposalActions(proposal);\n\t\tconst action = actions[Math.min(this.proposalChoice, actions.length - 1)];\n\t\tif (!action) return;\n\t\tthis.proposalAction = action.id;\n\t\tthis.proposalInput = \"\";\n\t\tthis.proposalError = undefined;\n\t\tif (action.id === \"approve\") {\n\t\t\tthis.proposalView = this.strictProposalDigest(proposal) ? \"digest\" : \"confirm\";\n\t\t} else if (action.id === \"reopen\") {\n\t\t\tvoid this.executeProposalAction(proposal, \"User reopened deferred proposal\");\n\t\t} else {\n\t\t\tthis.proposalView = \"reason\";\n\t\t}\n\t}\n\n\tprivate async executeProposalAction(proposal: Proposal, reason: string): Promise<void> {\n\t\tthis.proposalActing = true;\n\t\tthis.proposalError = undefined;\n\t\tthis.tui.requestRender();\n\t\ttry {\n\t\t\tlet notice: string;\n\t\t\tif (this.proposalAction === \"approve\") {\n\t\t\t\tconst approved = await this.service.approve(proposal.id, proposalApproval(proposal));\n\t\t\t\tnotice = `提案已批准（${approved.status === \"kept\" ? \"已直接生效\" : \"已进入 Trial\"}）：${proposal.id}`;\n\t\t\t} else if (this.proposalAction === \"reject\") {\n\t\t\t\tawait this.service.reject(proposal.id, reason);\n\t\t\t\tnotice = `提案已拒绝：${proposal.id}`;\n\t\t\t} else if (this.proposalAction === \"defer\") {\n\t\t\t\tconst deferred = await this.service.defer(proposal.id, reason);\n\t\t\t\tawait recordPermitDefer({\n\t\t\t\t\tpaths: this.paths,\n\t\t\t\t\tproposalId: deferred.id,\n\t\t\t\t\trevision: deferred.revision,\n\t\t\t\t\tdiffDigest: deferred.diffDigest,\n\t\t\t\t\treason,\n\t\t\t\t});\n\t\t\t\tnotice = `提案已推迟：${proposal.id}`;\n\t\t\t} else {\n\t\t\t\tconst reopened = await this.service.reopen(proposal.id, reason);\n\t\t\t\tawait recordPermitReopen({\n\t\t\t\t\tpaths: this.paths,\n\t\t\t\t\tproposalId: reopened.id,\n\t\t\t\t\trevision: reopened.revision,\n\t\t\t\t\tdiffDigest: reopened.diffDigest,\n\t\t\t\t\treason,\n\t\t\t\t});\n\t\t\t\tnotice = `提案已重新打开：${proposal.id}`;\n\t\t\t}\n\t\t\tthis.mode = \"tasks\";\n\t\t\tthis.itemKey = undefined;\n\t\t\tthis.item = undefined;\n\t\t\tthis.proposalView = \"info\";\n\t\t\tthis.taskNotice = notice;\n\t\t\tvoid this.refresh();\n\t\t} catch (error) {\n\t\t\tthis.proposalError = error instanceof Error ? error.message : String(error);\n\t\t} finally {\n\t\t\tthis.proposalActing = false;\n\t\t\tthis.tui.requestRender();\n\t\t}\n\t}\n\n\tprivate runLines(): string[] {\n\t\tif (this.item?.kind === \"proposal\") return this.proposalLines(this.item.proposal);\n\t\tconst run = this.run;\n\t\tif (!run) {\n\t\t\treturn [\n\t\t\t\tthis.theme.fg(\"warning\", \"正在连接后台任务……\"),\n\t\t\t\t...(this.loadError\n\t\t\t\t\t? [\"\", this.theme.fg(\"error\", this.loadError), this.theme.fg(\"dim\", \"Esc 返回事项列表\")]\n\t\t\t\t\t: []),\n\t\t\t];\n\t\t}\n\t\tif (run.status === \"awaiting-canary-approval\") return this.canaryLines(run);\n\t\tif (run.status === \"trialing\" && this.item?.component) return this.activeCanaryLines();\n\t\tconst thinking = thinkingText(this.events);\n\t\tconst tools = toolEvents(this.events);\n\t\tconst outcomes = [\n\t\t\tthis.artifacts.plan && \"研究计划\",\n\t\t\tthis.artifacts.experiment && \"冻结实验\",\n\t\t\trun.proposalId && \"候选提案\",\n\t\t\tthis.artifacts.evaluation && \"评估报告\",\n\t\t].filter(Boolean) as string[];\n\t\tconst toolContent: string[] = [];\n\t\tfor (const [index, entry] of tools.entries()) {\n\t\t\ttoolContent.push(`${index + 1}. ${toolDescription(entry.call)}`);\n\t\t\tif (entry.result) {\n\t\t\t\ttoolContent.push(\n\t\t\t\t\tthis.theme.fg(\n\t\t\t\t\t\tentry.result.isError ? \"error\" : \"success\",\n\t\t\t\t\t\t`   ${entry.result.isError ? \"失败\" : \"完成\"}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tif (entry.result.text)\n\t\t\t\t\ttoolContent.push(\n\t\t\t\t\t\t...entry.result.text\n\t\t\t\t\t\t\t.split(\"\\n\")\n\t\t\t\t\t\t\t.slice(0, 12)\n\t\t\t\t\t\t\t.map((line) => this.theme.fg(\"dim\", `   ${line}`)),\n\t\t\t\t\t);\n\t\t\t} else toolContent.push(this.theme.fg(\"warning\", \"   运行中\"));\n\t\t}\n\t\tif (tools.length === 0) toolContent.push(\"尚无工具调用\");\n\t\tconst outcomeContent: string[] = [];\n\t\tif (this.artifacts.plan)\n\t\t\toutcomeContent.push(\n\t\t\t\tthis.theme.fg(\"success\", \"✓ 研究计划\"),\n\t\t\t\t...this.artifacts.plan.split(\"\\n\").map((line) => `  ${line}`),\n\t\t\t);\n\t\tif (this.artifacts.experiment)\n\t\t\toutcomeContent.push(\n\t\t\t\tthis.theme.fg(\"success\", \"✓ 冻结实验\"),\n\t\t\t\t...this.artifacts.experiment.split(\"\\n\").map((line) => `  ${line}`),\n\t\t\t);\n\t\tif (run.proposalId) outcomeContent.push(this.theme.fg(\"success\", \"✓ 候选提案\"));\n\t\tif (this.artifacts.evaluation)\n\t\t\toutcomeContent.push(\n\t\t\t\tthis.theme.fg(\"success\", \"✓ 评估报告\"),\n\t\t\t\t...this.artifacts.evaluation.split(\"\\n\").map((line) => `  ${line}`),\n\t\t\t);\n\t\tif (outcomes.length === 0) outcomeContent.push(\"当前阶段尚未生成可审阅成果\");\n\t\tconst lines = [\n\t\t\tthis.theme.fg(\"accent\", this.theme.bold(\"Evo 工作流进度\")),\n\t\t\t\"\",\n\t\t\t...(this.item?.component ? [`${this.theme.bold(\"Component\")}  ${this.item.component}`] : []),\n\t\t\t`${this.theme.bold(\"目标\")}  ${run.request ?? \"定时演化\"}`,\n\t\t\t`${this.theme.bold(\"状态\")}  ${phaseLabel(run.status)} · ${elapsed(run.startedAt)}`,\n\t\t\t\"\",\n\t\t\tstageProgress(this.theme, run.status),\n\t\t\t\"\",\n\t\t\t`${this.theme.bold(\"当前活动\")}  ${currentActivity(this.events, run.status)}`,\n\t\t\t\"\",\n\t\t];\n\t\tthis.pushSections(lines, [\n\t\t\t{\n\t\t\t\tid: \"thinking\",\n\t\t\t\ttitle: \"当前思考\",\n\t\t\t\tsummary: thinking ? `${thinking.length} 字符` : \"尚无输出\",\n\t\t\t\tcontent: thinking ? thinking.split(\"\\n\").map((line) => this.theme.fg(\"dim\", line)) : [\"尚无 thinking 输出\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"tools\",\n\t\t\t\ttitle: \"工具调用\",\n\t\t\t\tsummary: tools.length > 0 ? `${tools.length} 次调用` : \"尚未调用\",\n\t\t\t\tcontent: toolContent,\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"outcomes\",\n\t\t\t\ttitle: \"阶段成果\",\n\t\t\t\tsummary: outcomes.length > 0 ? outcomes.join(\"、\") : \"尚无阶段成果\",\n\t\t\t\tcontent: outcomeContent,\n\t\t\t},\n\t\t]);\n\t\tif (run.error) lines.push(\"\", this.theme.fg(\"error\", `错误：${run.error}`));\n\t\treturn lines;\n\t}\n\n\trender(width: number): string[] {\n\t\tthis.focusedSectionLine = -1;\n\t\tconst logical = this.mode === \"tasks\" ? this.taskLines() : this.runLines();\n\t\t// Decision controls and key hints stay pinned below the scrollable\n\t\t// content so the next action is always visible to the user.\n\t\tconst pinned = this.pinnedLines().flatMap((line) =>\n\t\t\twrapTextWithAnsi(line, Math.max(1, width - 2)).map((part) => ` ${part}`),\n\t\t);\n\t\tconst wrapped: string[] = [];\n\t\tlet selectedStart = 0;\n\t\tlet selectedEnd = 0;\n\t\tlet focusedStart = -1;\n\t\tfor (const [index, line] of logical.entries()) {\n\t\t\tif (this.mode === \"tasks\" && index === this.selectedTaskLine) selectedStart = wrapped.length;\n\t\t\tif (index === this.focusedSectionLine) focusedStart = wrapped.length;\n\t\t\tfor (const part of wrapTextWithAnsi(line, Math.max(1, width - 2))) wrapped.push(` ${part}`);\n\t\t\tif (this.mode === \"tasks\" && index === this.selectedTaskLine) selectedEnd = wrapped.length;\n\t\t}\n\t\tconst viewport = Math.max(5, this.tui.terminal.rows - 4 - pinned.length);\n\t\tconst maxStart = Math.max(0, wrapped.length - viewport);\n\t\tif (this.mode === \"tasks\") {\n\t\t\t// The task list is top-anchored and scrolls only as far as needed to\n\t\t\t// keep the selected item visible; the highest-priority items are at\n\t\t\t// the top, so a small terminal must never hide them by default.\n\t\t\tif (selectedEnd > this.taskScrollTop + viewport) this.taskScrollTop = selectedEnd - viewport;\n\t\t\tif (selectedStart < this.taskScrollTop) this.taskScrollTop = selectedStart;\n\t\t\tthis.taskScrollTop = Math.min(Math.max(0, this.taskScrollTop), maxStart);\n\t\t\treturn [...wrapped.slice(this.taskScrollTop, this.taskScrollTop + viewport), ...pinned];\n\t\t}\n\t\tlet start = Math.max(0, maxStart - this.scrollFromBottom);\n\t\tif (this.followFocus && focusedStart >= 0) {\n\t\t\tif (focusedStart < start) start = focusedStart;\n\t\t\telse if (focusedStart >= start + viewport) start = focusedStart - viewport + 1;\n\t\t\tstart = Math.min(Math.max(0, start), maxStart);\n\t\t\tthis.scrollFromBottom = maxStart - start;\n\t\t}\n\t\tthis.followFocus = false;\n\t\treturn [...wrapped.slice(start, start + viewport), ...pinned];\n\t}\n\n\tprivate activateApprovalChoice(choice: number): void {\n\t\tif (this.approving) return;\n\t\tthis.approvalChoice = choice;\n\t\tif (choice === 0) this.startComponentApproval({ mode: \"canary\", customization: \"default\" });\n\t\telse if (choice === 1) this.approvalView = \"custom\";\n\t\telse if (this.proposal?.artifacts.validation) this.approvalView = \"direct-confirm\";\n\t\telse this.approvalError = \"直接上线要求通过并绑定可执行 validation artifact\";\n\t}\n\n\tprivate startComponentApproval(decision: ComponentApprovalDecision): void {\n\t\tif (!this.run || this.approving) return;\n\t\tconst proposal = this.proposal;\n\t\tif (\n\t\t\t!proposal ||\n\t\t\tproposal.status !== \"pending\" ||\n\t\t\tproposal.parentBundleDigest !== this.run.canaryParentDigest ||\n\t\t\tproposal.candidateDigest !== this.run.canaryCandidateDigest ||\n\t\t\tproposal.targetAbi !== this.run.canaryTargetAbi\n\t\t) {\n\t\t\tthis.approvalError = \"运行记录与当前提案不一致\";\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tthis.approving = true;\n\t\tthis.approvalError = undefined;\n\t\tvoid this.approveCanary(this.run.id, decision)\n\t\t\t.then(() => this.refresh())\n\t\t\t.catch((error) => {\n\t\t\t\tthis.approvalError = error instanceof Error ? error.message : String(error);\n\t\t\t})\n\t\t\t.finally(() => {\n\t\t\t\tthis.approving = false;\n\t\t\t\tthis.tui.requestRender();\n\t\t\t});\n\t}\n\n\tprivate startDirectKeep(): void {\n\t\tif (!this.run || this.keeping) return;\n\t\tif (!this.proposal?.artifacts.validation) {\n\t\t\tthis.keepError = \"正式上线要求绑定可执行 validation artifact\";\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tthis.keeping = true;\n\t\tthis.keepError = undefined;\n\t\tvoid this.directKeepCanary(this.run.id)\n\t\t\t.then(() => this.refresh())\n\t\t\t.catch((error) => {\n\t\t\t\tthis.keepError = error instanceof Error ? error.message : String(error);\n\t\t\t})\n\t\t\t.finally(() => {\n\t\t\t\tthis.keeping = false;\n\t\t\t\tthis.tui.requestRender();\n\t\t\t});\n\t}\n\n\thandleInput(data: string): void {\n\t\tif (\n\t\t\tthis.mode === \"run\" &&\n\t\t\tthis.run?.status === \"trialing\" &&\n\t\t\tthis.keepConfirming &&\n\t\t\tmatchesKey(data, Key.escape)\n\t\t) {\n\t\t\tthis.keepConfirming = false;\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (\n\t\t\tthis.mode === \"run\" &&\n\t\t\tthis.run?.status === \"awaiting-canary-approval\" &&\n\t\t\tthis.approvalView !== \"choice\" &&\n\t\t\tmatchesKey(data, Key.escape)\n\t\t) {\n\t\t\tthis.approvalView = \"choice\";\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tconst proposalTextEntry =\n\t\t\tthis.mode === \"run\" &&\n\t\t\tthis.item?.kind === \"proposal\" &&\n\t\t\t(this.proposalView === \"reason\" || this.proposalView === \"digest\");\n\t\tif (this.mode === \"run\" && this.item?.kind === \"proposal\" && this.proposalView !== \"info\") {\n\t\t\tif (matchesKey(data, Key.escape)) {\n\t\t\t\tthis.proposalView = \"info\";\n\t\t\t\tthis.proposalInput = \"\";\n\t\t\t\tthis.proposalError = undefined;\n\t\t\t\tthis.tui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (matchesKey(data, Key.escape) || (data === \"q\" && !proposalTextEntry)) {\n\t\t\tif (this.mode === \"run\") {\n\t\t\t\tthis.mode = \"tasks\";\n\t\t\t\tthis.itemKey = undefined;\n\t\t\t\tthis.item = undefined;\n\t\t\t\tthis.scrollFromBottom = 0;\n\t\t\t\tthis.proposalView = \"info\";\n\t\t\t\tthis.proposalInput = \"\";\n\t\t\t\tthis.proposalError = undefined;\n\t\t\t} else this.close();\n\t\t\tthis.tui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tif (this.mode === \"tasks\") {\n\t\t\tif (matchesKey(data, Key.up)) this.taskIndex = Math.max(0, this.taskIndex - 1);\n\t\t\telse if (matchesKey(data, Key.down))\n\t\t\t\tthis.taskIndex = Math.min(Math.max(0, this.displayItems.length - 1), this.taskIndex + 1);\n\t\t\telse if (matchesKey(data, Key.enter)) {\n\t\t\t\tconst selected = this.displayItems[this.taskIndex];\n\t\t\t\tif (selected) {\n\t\t\t\t\tthis.itemKey = selected.key;\n\t\t\t\t\tthis.item = selected;\n\t\t\t\t\tthis.run = selected.kind === \"run\" ? selected.run : undefined;\n\t\t\t\t\tthis.mode = \"run\";\n\t\t\t\t\tthis.taskNotice = undefined;\n\t\t\t\t\tthis.sectionFocus = 0;\n\t\t\t\t\tthis.expandedSections = new Set();\n\t\t\t\t\tthis.followFocus = false;\n\t\t\t\t\t// Proposal cards are static documents and open at the top; run\n\t\t\t\t\t// views stay bottom-anchored to follow live progress.\n\t\t\t\t\tthis.scrollFromBottom = selected.kind === \"proposal\" ? Number.MAX_SAFE_INTEGER : 0;\n\t\t\t\t\tthis.approvalView = \"choice\";\n\t\t\t\t\tthis.approvalChoice = 0;\n\t\t\t\t\tthis.customField = 0;\n\t\t\t\t\tthis.customDefaultsInitialized = false;\n\t\t\t\t\tthis.keepConfirming = false;\n\t\t\t\t\tthis.keepError = undefined;\n\t\t\t\t\tvoid this.refresh();\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif (this.item?.kind === \"proposal\") {\n\t\t\t\tconst proposal = this.item.proposal;\n\t\t\t\tif (this.proposalView === \"reason\" || this.proposalView === \"digest\") {\n\t\t\t\t\tif (matchesKey(data, Key.enter)) {\n\t\t\t\t\t\tif (this.proposalActing) return;\n\t\t\t\t\t\tconst input = this.proposalInput.trim();\n\t\t\t\t\t\tif (this.proposalView === \"digest\") {\n\t\t\t\t\t\t\tif (input !== this.strictProposalDigest(proposal)) {\n\t\t\t\t\t\t\t\tthis.proposalError = \"digest 不匹配；请对照上方需要输入的值\";\n\t\t\t\t\t\t\t\tthis.tui.requestRender();\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tvoid this.executeProposalAction(proposal, `Approved exact digest ${input}`);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (!input) {\n\t\t\t\t\t\t\tthis.proposalError = \"理由不能为空\";\n\t\t\t\t\t\t\tthis.tui.requestRender();\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tvoid this.executeProposalAction(proposal, input);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif (matchesKey(data, Key.backspace)) this.proposalInput = this.proposalInput.slice(0, -1);\n\t\t\t\t\telse if (!data.startsWith(\"\\u001b\")) {\n\t\t\t\t\t\t// Printable characters and pasted chunks; control bytes dropped.\n\t\t\t\t\t\tconst clean = data.replace(/[\\u0000-\\u001f\\u007f]/g, \"\");\n\t\t\t\t\t\tthis.proposalInput = `${this.proposalInput}${clean}`.slice(0, 256);\n\t\t\t\t\t}\n\t\t\t\t\tthis.tui.requestRender();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (this.proposalView === \"confirm\") {\n\t\t\t\t\tif (matchesKey(data, Key.enter) && !this.proposalActing) {\n\t\t\t\t\t\tvoid this.executeProposalAction(proposal, `Approved exact diff ${proposal.diffDigest}`);\n\t\t\t\t\t}\n\t\t\t\t\tthis.tui.requestRender();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst actions = this.availableProposalActions(proposal);\n\t\t\t\tif (matchesKey(data, Key.left)) this.proposalChoice = Math.max(0, this.proposalChoice - 1);\n\t\t\t\telse if (matchesKey(data, Key.right)) {\n\t\t\t\t\tthis.proposalChoice = Math.min(Math.max(0, actions.length - 1), this.proposalChoice + 1);\n\t\t\t\t} else if (matchesKey(data, Key.up)) this.moveSectionFocus(-1);\n\t\t\t\telse if (matchesKey(data, Key.down)) this.moveSectionFocus(1);\n\t\t\t\telse if (matchesKey(data, Key.home)) this.scrollFromBottom = Number.MAX_SAFE_INTEGER;\n\t\t\t\telse if (matchesKey(data, Key.end)) this.scrollFromBottom = 0;\n\t\t\t\telse if (matchesKey(data, Key.tab)) this.focusNextSection();\n\t\t\t\telse if (matchesKey(data, Key.space)) this.toggleFocusedSection();\n\t\t\t\telse if (matchesKey(data, Key.enter) && !this.proposalActing) {\n\t\t\t\t\tif (actions.length > 0) this.startProposalAction(proposal);\n\t\t\t\t\telse this.toggleFocusedSection();\n\t\t\t\t} else if (data.length === 1 && !data.startsWith(\"\\u001b\") && !this.proposalActing) {\n\t\t\t\t\tconst hotkeyIndex = actions.findIndex((action) => PROPOSAL_ACTION_HOTKEYS[action.id] === data);\n\t\t\t\t\tif (hotkeyIndex >= 0) {\n\t\t\t\t\t\tthis.proposalChoice = hotkeyIndex;\n\t\t\t\t\t\tthis.startProposalAction(proposal);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthis.tui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (this.run?.status === \"trialing\" && this.item?.component) {\n\t\t\t\tif (matchesKey(data, Key.up)) this.scrollFromBottom++;\n\t\t\t\telse if (matchesKey(data, Key.down)) this.scrollFromBottom = Math.max(0, this.scrollFromBottom - 1);\n\t\t\t\telse if (matchesKey(data, Key.home)) this.scrollFromBottom = Number.MAX_SAFE_INTEGER;\n\t\t\t\telse if (matchesKey(data, Key.end)) this.scrollFromBottom = 0;\n\t\t\t\telse if (matchesKey(data, Key.tab)) this.focusNextSection();\n\t\t\t\telse if (matchesKey(data, Key.space)) this.toggleFocusedSection();\n\t\t\t\telse if (matchesKey(data, Key.enter) && !this.keeping) {\n\t\t\t\t\tif (this.keepConfirming) this.startDirectKeep();\n\t\t\t\t\telse this.keepConfirming = true;\n\t\t\t\t}\n\t\t\t\tthis.tui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (this.run?.status === \"awaiting-evidence\" && this.pendingVerification) {\n\t\t\t\tif (matchesKey(data, Key.left)) this.verificationChoice = Math.max(0, this.verificationChoice - 1);\n\t\t\t\telse if (matchesKey(data, Key.right)) this.verificationChoice = Math.min(2, this.verificationChoice + 1);\n\t\t\t\telse if (matchesKey(data, Key.up)) this.scrollFromBottom++;\n\t\t\t\telse if (matchesKey(data, Key.down)) this.scrollFromBottom = Math.max(0, this.scrollFromBottom - 1);\n\t\t\t\telse if (matchesKey(data, Key.home)) this.scrollFromBottom = Number.MAX_SAFE_INTEGER;\n\t\t\t\telse if (matchesKey(data, Key.end)) this.scrollFromBottom = 0;\n\t\t\t\telse if (matchesKey(data, Key.tab)) this.focusNextSection();\n\t\t\t\telse if (matchesKey(data, Key.space)) this.toggleFocusedSection();\n\t\t\t\telse if (matchesKey(data, Key.enter)) {\n\t\t\t\t\tthis.startVerificationDecision(\n\t\t\t\t\t\tthis.verificationChoice === 0 ? \"execute\" : this.verificationChoice === 1 ? \"skip\" : \"reject\",\n\t\t\t\t\t);\n\t\t\t\t} else if (data === \"e\" || data === \"s\" || data === \"r\") {\n\t\t\t\t\tthis.verificationChoice = data === \"e\" ? 0 : data === \"s\" ? 1 : 2;\n\t\t\t\t\tthis.startVerificationDecision(data === \"e\" ? \"execute\" : data === \"s\" ? \"skip\" : \"reject\");\n\t\t\t\t}\n\t\t\t\tthis.tui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (this.run?.status === \"awaiting-canary-approval\") {\n\t\t\t\tif (this.approvalView === \"custom\") {\n\t\t\t\t\tif (matchesKey(data, Key.up)) this.customField = Math.max(0, this.customField - 1);\n\t\t\t\t\telse if (matchesKey(data, Key.down)) this.customField = Math.min(2, this.customField + 1);\n\t\t\t\t\telse if (matchesKey(data, Key.left) && this.customField === 0)\n\t\t\t\t\t\tthis.customMinimumSamples = Math.max(1, this.customMinimumSamples - 1);\n\t\t\t\t\telse if (matchesKey(data, Key.right) && this.customField === 0)\n\t\t\t\t\t\tthis.customMinimumSamples = Math.min(10_000, this.customMinimumSamples + 1);\n\t\t\t\t\telse if (matchesKey(data, Key.left) && this.customField === 1)\n\t\t\t\t\t\tthis.customMaximumDurationDays = Math.max(1, this.customMaximumDurationDays - 1);\n\t\t\t\t\telse if (matchesKey(data, Key.right) && this.customField === 1)\n\t\t\t\t\t\tthis.customMaximumDurationDays = Math.min(365, this.customMaximumDurationDays + 1);\n\t\t\t\t\telse if (matchesKey(data, Key.enter) && this.customField === 2) {\n\t\t\t\t\t\tthis.startComponentApproval({\n\t\t\t\t\t\t\tmode: \"canary\",\n\t\t\t\t\t\t\tcustomization: \"custom\",\n\t\t\t\t\t\t\tminimumSamples: this.customMinimumSamples,\n\t\t\t\t\t\t\tmaximumDurationDays: this.customMaximumDurationDays,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t} else if (this.approvalView === \"direct-confirm\") {\n\t\t\t\t\tif (matchesKey(data, Key.enter)) this.startComponentApproval({ mode: \"direct\" });\n\t\t\t\t} else if (matchesKey(data, Key.left)) {\n\t\t\t\t\tthis.approvalChoice = Math.max(0, this.approvalChoice - 1);\n\t\t\t\t} else if (matchesKey(data, Key.right)) {\n\t\t\t\t\tthis.approvalChoice = Math.min(2, this.approvalChoice + 1);\n\t\t\t\t} else if (matchesKey(data, Key.up)) this.scrollFromBottom++;\n\t\t\t\telse if (matchesKey(data, Key.down)) this.scrollFromBottom = Math.max(0, this.scrollFromBottom - 1);\n\t\t\t\telse if (matchesKey(data, Key.home)) this.scrollFromBottom = Number.MAX_SAFE_INTEGER;\n\t\t\t\telse if (matchesKey(data, Key.end)) this.scrollFromBottom = 0;\n\t\t\t\telse if (matchesKey(data, Key.tab)) this.focusNextSection();\n\t\t\t\telse if (matchesKey(data, Key.space)) this.toggleFocusedSection();\n\t\t\t\telse if (matchesKey(data, Key.enter)) this.activateApprovalChoice(this.approvalChoice);\n\t\t\t\telse if (data === \"c\" || data === \"u\" || data === \"x\") {\n\t\t\t\t\tthis.activateApprovalChoice(data === \"c\" ? 0 : data === \"u\" ? 1 : 2);\n\t\t\t\t}\n\t\t\t\tthis.tui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (matchesKey(data, Key.up)) this.scrollFromBottom++;\n\t\t\telse if (matchesKey(data, Key.down)) this.scrollFromBottom = Math.max(0, this.scrollFromBottom - 1);\n\t\t\telse if (matchesKey(data, Key.tab)) this.focusNextSection();\n\t\t\telse if (matchesKey(data, Key.space) || matchesKey(data, Key.enter)) this.toggleFocusedSection();\n\t\t\tif (matchesKey(data, Key.home)) this.scrollFromBottom = Number.MAX_SAFE_INTEGER;\n\t\t\telse if (matchesKey(data, Key.end)) this.scrollFromBottom = 0;\n\t\t}\n\t\tthis.tui.requestRender();\n\t}\n\n\tinvalidate(): void {}\n\n\tdispose(): void {\n\t\tclearInterval(this.timer);\n\t}\n}\n"]}