@miphamai/cli 0.63.0 → 0.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/standard/safe-coding.SKILL.md +9 -0
- package/src/config/defaults.ts +4 -0
- package/src/core/autocomplete.ts +64 -0
- package/src/core/crsi-modify.ts +13 -11
- package/src/core/crsi-producer.ts +128 -2
- package/src/core/crsi-sandbox.ts +60 -22
- package/src/core/eval-harness.ts +23 -8
- package/src/core/improvement-track.ts +144 -0
- package/src/core/reward-fn.ts +44 -0
- package/src/core/task-performance-tasks.json +42 -0
- package/src/core/task-performance.ts +244 -0
- package/src/i18n-core/locales/en-US.json +3 -1
- package/src/i18n-core/locales/zh-CN.json +3 -1
- package/src/shared/package-info.ts +1 -1
- package/src/shared/types.ts +7 -0
- package/src/shared/update.ts +41 -0
- package/src/skills/bundled-skills.ts +1 -0
- package/src/skills/loader.ts +1 -1
- package/src/ui/agent-footer.tsx +5 -1
- package/src/ui/app.tsx +71 -3
- package/src/ui/commands.ts +162 -8
- package/src/ui/input.tsx +71 -0
package/src/ui/commands.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { MiphamConfig } from '../shared/index.ts'
|
|
|
9
9
|
import type { SkillsLoader } from '../skills/loader'
|
|
10
10
|
import type { PluginManager } from '../plugin/plugin-manager'
|
|
11
11
|
import type { Message } from '../shared/types.js'
|
|
12
|
+
import type { UpdateStatus } from '../shared/update'
|
|
12
13
|
import { McpClient } from '../mcp/client'
|
|
13
14
|
import { buildCapabilityReport } from '../core/capability-inventory'
|
|
14
15
|
import { InstructionsLoader } from '../core/instructions'
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
produceCrsiProposal,
|
|
19
20
|
produceRuleProposal,
|
|
20
21
|
produceProseProposal,
|
|
22
|
+
produceCrossoverProposal,
|
|
21
23
|
selectCrsiSignal,
|
|
22
24
|
collectSkillFiles,
|
|
23
25
|
proseProposalId,
|
|
@@ -29,6 +31,18 @@ import {
|
|
|
29
31
|
} from '../core/crsi-producer'
|
|
30
32
|
import { prefilterProposal } from '../core/proposal-guard'
|
|
31
33
|
import { runEval, appendEvalScore } from '../core/eval-harness'
|
|
34
|
+
import { listRewardFns } from '../core/reward-fn'
|
|
35
|
+
import { runTaskPerformance, measureSkillDeltaRepeated } from '../core/task-performance'
|
|
36
|
+
import { randomUUID } from 'node:crypto'
|
|
37
|
+
import {
|
|
38
|
+
buildImprovementReport,
|
|
39
|
+
appendImprovement,
|
|
40
|
+
readImprovements,
|
|
41
|
+
improvementRate,
|
|
42
|
+
setPendingVerdict,
|
|
43
|
+
getPendingVerdict,
|
|
44
|
+
shouldBlockApproval,
|
|
45
|
+
} from '../core/improvement-track'
|
|
32
46
|
import { NPM_UPDATE_COMMAND, PACKAGE_VERSION, COAUTHOR_TRAILER } from '../shared/index.ts'
|
|
33
47
|
import { getPreference } from '../config/preferences'
|
|
34
48
|
import { loadCrossSessionConfig } from '../config/loader'
|
|
@@ -81,6 +95,7 @@ export interface CommandContext {
|
|
|
81
95
|
setFocusMode: (on: boolean) => void
|
|
82
96
|
setGoal: (text: string) => void
|
|
83
97
|
setUltracodeMode: (on: boolean) => void
|
|
98
|
+
setUpdateStatus: (s: UpdateStatus) => void
|
|
84
99
|
skillsLoader?: SkillsLoader
|
|
85
100
|
pluginManager?: PluginManager
|
|
86
101
|
/** i18n translate function — populated from React tree via useI18n().
|
|
@@ -761,11 +776,16 @@ const crsiInventoryCmd: CommandHandler = async (ctx) => {
|
|
|
761
776
|
|
|
762
777
|
const crsiModifyCmd: CommandHandler = async (ctx, args) => {
|
|
763
778
|
if (args[0] === '--approve') {
|
|
779
|
+
if (shouldBlockApproval(getPendingVerdict() ?? 'inconclusive')) {
|
|
780
|
+
return { content: '❌ 任务表现倒退,禁止固化。请 /crsi modify --reject 丢弃,或改进后再试。' }
|
|
781
|
+
}
|
|
764
782
|
const r = approvePending()
|
|
783
|
+
setPendingVerdict(null)
|
|
765
784
|
return { content: r.success ? `✅ ${r.message}` : `⚠️ ${r.message}` }
|
|
766
785
|
}
|
|
767
786
|
if (args[0] === '--reject') {
|
|
768
787
|
const r = rejectPending()
|
|
788
|
+
setPendingVerdict(null)
|
|
769
789
|
return { content: r.success ? `✅ ${r.message}` : `⚠️ ${r.message}` }
|
|
770
790
|
}
|
|
771
791
|
if (args.length < 3) {
|
|
@@ -794,17 +814,50 @@ const crsiModifyCmd: CommandHandler = async (ctx, args) => {
|
|
|
794
814
|
// 文件不存在 → 宽松模式(originalContent 为空)
|
|
795
815
|
}
|
|
796
816
|
|
|
797
|
-
const result = runCrsiModification({
|
|
817
|
+
const result = await runCrsiModification({
|
|
818
|
+
description,
|
|
819
|
+
filePath,
|
|
820
|
+
newContent,
|
|
821
|
+
originalContent,
|
|
822
|
+
blastRadius: [filePath],
|
|
823
|
+
})
|
|
798
824
|
if (!result.applied || result.phase === 'failed') {
|
|
799
825
|
return {
|
|
800
826
|
content: `❌ 修改未通过(phase: ${result.phase})。\n${result.error ?? ''}`,
|
|
801
827
|
}
|
|
802
828
|
}
|
|
803
829
|
|
|
830
|
+
// 测量在 runCrsiModification 成功后进行(避免 failed proposal 白跑 6 次 LLM 调用)。
|
|
831
|
+
const llm = ctx.engine.getLlm() ?? ctx.engine.getRegistry()
|
|
832
|
+
let improvementLine = ''
|
|
833
|
+
try {
|
|
834
|
+
const sample = await measureSkillDeltaRepeated(llm, { filePath, originalContent, newContent })
|
|
835
|
+
if (sample) {
|
|
836
|
+
const report = buildImprovementReport(sample, [filePath])
|
|
837
|
+
setPendingVerdict(report.verdict)
|
|
838
|
+
appendImprovement({ ...report, id: randomUUID(), timestamp: new Date().toISOString() })
|
|
839
|
+
const rate = improvementRate(readImprovements())
|
|
840
|
+
const label =
|
|
841
|
+
report.verdict === 'improved'
|
|
842
|
+
? 'improved ✅'
|
|
843
|
+
: report.verdict === 'regressed'
|
|
844
|
+
? 'regressed ⚠️'
|
|
845
|
+
: 'inconclusive'
|
|
846
|
+
const sign = report.deltaMean >= 0 ? '+' : ''
|
|
847
|
+
improvementLine =
|
|
848
|
+
`\n📊 改进判定: ${label} (delta ${sign}${report.deltaMean.toFixed(1)}, 噪声 ${report.noise.toFixed(1)}, 阈值 ${report.minEffect.toFixed(1)})` +
|
|
849
|
+
`\n 改进率: ${rate.improved}/${rate.total} (${(rate.rate * 100).toFixed(0)}%, Wilson 95% [${(rate.lo * 100).toFixed(0)}%, ${(rate.hi * 100).toFixed(0)}%])` +
|
|
850
|
+
(report.verdict === 'regressed' ? '\n ⚠️ 任务表现倒退:--approve 将被拒绝。' : '')
|
|
851
|
+
}
|
|
852
|
+
} catch {
|
|
853
|
+
// 测量失败(LLM 不可用等)不阻断 modify 流程——改进信号是可选的。
|
|
854
|
+
}
|
|
855
|
+
|
|
804
856
|
return {
|
|
805
857
|
content:
|
|
806
|
-
`✅ 测试通过。审阅下方 diff:\n\n${result.diff}\n
|
|
807
|
-
|
|
858
|
+
`✅ 测试通过。审阅下方 diff:\n\n${result.diff}\n` +
|
|
859
|
+
improvementLine +
|
|
860
|
+
'\n/crsi modify --approve 合并\n/crsi modify --reject 丢弃',
|
|
808
861
|
}
|
|
809
862
|
}
|
|
810
863
|
|
|
@@ -869,11 +922,12 @@ const crsiProposeCmd: CommandHandler = async (ctx, args) => {
|
|
|
869
922
|
return { content: `❌ 提议未通过预筛:${verdict.reasons.join('; ')}` }
|
|
870
923
|
}
|
|
871
924
|
|
|
872
|
-
const result = runCrsiModification({
|
|
925
|
+
const result = await runCrsiModification({
|
|
873
926
|
description: proposal.description,
|
|
874
927
|
filePath: proposal.filePath,
|
|
875
928
|
newContent: proposal.newContent,
|
|
876
929
|
originalContent: proposal.originalContent,
|
|
930
|
+
blastRadius: [proposal.filePath],
|
|
877
931
|
})
|
|
878
932
|
if (!result.applied || result.phase === 'failed') {
|
|
879
933
|
return { content: `❌ 生成失败(phase: ${result.phase})。\n${result.error ?? ''}` }
|
|
@@ -911,7 +965,7 @@ const crsiProposeCmd: CommandHandler = async (ctx, args) => {
|
|
|
911
965
|
}
|
|
912
966
|
}
|
|
913
967
|
|
|
914
|
-
const result = runCrsiModification(proposal)
|
|
968
|
+
const result = await runCrsiModification(proposal)
|
|
915
969
|
if (!result.applied || result.phase === 'failed') {
|
|
916
970
|
return { content: `❌ 固化失败(phase: ${result.phase})。\n${result.error ?? ''}` }
|
|
917
971
|
}
|
|
@@ -923,6 +977,38 @@ const crsiProposeCmd: CommandHandler = async (ctx, args) => {
|
|
|
923
977
|
}
|
|
924
978
|
}
|
|
925
979
|
|
|
980
|
+
// ── Crossover 路径:/crsi propose --crossover 合并两条重叠教训 ──
|
|
981
|
+
if (args[0] === '--crossover') {
|
|
982
|
+
const llm = ctx.engine.getLlm() ?? ctx.engine.getRegistry()
|
|
983
|
+
let current = ''
|
|
984
|
+
try {
|
|
985
|
+
const { readFileSync } = await import('node:fs')
|
|
986
|
+
const { join } = await import('node:path')
|
|
987
|
+
current = readFileSync(join(root, LESSONS_FILE), 'utf-8')
|
|
988
|
+
} catch {
|
|
989
|
+
current = ''
|
|
990
|
+
}
|
|
991
|
+
if (!current) {
|
|
992
|
+
return { content: '教训文件为空,无可合并。' }
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
const proposal = await produceCrossoverProposal(llm, current, new Date().toISOString())
|
|
996
|
+
if (!proposal) {
|
|
997
|
+
return { content: '没有找到可合并的重叠教训对。' }
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const result = await runCrsiModification(proposal)
|
|
1001
|
+
if (!result.applied || result.phase === 'failed') {
|
|
1002
|
+
return { content: `❌ 合并失败(phase: ${result.phase})。\n${result.error ?? ''}` }
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
return {
|
|
1006
|
+
content:
|
|
1007
|
+
`✅ 已生成合并教训并跑过测试。审阅 diff:\n\n${result.diff}\n\n` +
|
|
1008
|
+
'/crsi modify --approve 合并 | /crsi modify --reject 丢弃',
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
926
1012
|
// ── 教训路径(默认):/crsi propose 追加教训 ──
|
|
927
1013
|
let current = ''
|
|
928
1014
|
try {
|
|
@@ -939,7 +1025,7 @@ const crsiProposeCmd: CommandHandler = async (ctx, args) => {
|
|
|
939
1025
|
return { content: '没有足够的失败信号(autoApplicable insight 或高置信元规则)来生成教训。' }
|
|
940
1026
|
}
|
|
941
1027
|
|
|
942
|
-
const result = runCrsiModification(proposal)
|
|
1028
|
+
const result = await runCrsiModification(proposal)
|
|
943
1029
|
if (!result.applied || result.phase === 'failed') {
|
|
944
1030
|
return { content: `❌ 生成失败(phase: ${result.phase})。\n${result.error ?? ''}` }
|
|
945
1031
|
}
|
|
@@ -951,9 +1037,28 @@ const crsiProposeCmd: CommandHandler = async (ctx, args) => {
|
|
|
951
1037
|
}
|
|
952
1038
|
}
|
|
953
1039
|
|
|
954
|
-
const crsiEvalCmd: CommandHandler = async () => {
|
|
1040
|
+
const crsiEvalCmd: CommandHandler = async (ctx, args) => {
|
|
1041
|
+
const rewardIdx = args.indexOf('--reward')
|
|
1042
|
+
const rewardName = rewardIdx >= 0 ? args[rewardIdx + 1] : undefined
|
|
1043
|
+
|
|
1044
|
+
if (rewardName) {
|
|
1045
|
+
const llm = ctx.engine.getLlm() ?? ctx.engine.getRegistry()
|
|
1046
|
+
const fns = listRewardFns(llm)
|
|
1047
|
+
const fn = fns.find((f) => f.name === rewardName)
|
|
1048
|
+
if (!fn) {
|
|
1049
|
+
return {
|
|
1050
|
+
content: `❌ 未知 reward: ${rewardName}。可用: ${fns.map((f) => f.name).join(', ')}`,
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
const report = await fn.evaluate()
|
|
1054
|
+
appendEvalScore(fn.name, report)
|
|
1055
|
+
return {
|
|
1056
|
+
content: `得分 **${report.score}/100** (${report.passed}/${report.total})\n失败: ${report.failures.join(', ') || '无'}`,
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
955
1060
|
const report = runEval()
|
|
956
|
-
appendEvalScore(report)
|
|
1061
|
+
appendEvalScore('mechanism-sentinel', report)
|
|
957
1062
|
|
|
958
1063
|
const lines: string[] = ['## 🧪 CRSI Eval Harness', '']
|
|
959
1064
|
lines.push(`得分: **${report.score}/100** (${report.passed}/${report.total})`, '')
|
|
@@ -967,6 +1072,51 @@ const crsiEvalCmd: CommandHandler = async () => {
|
|
|
967
1072
|
if (report.failures.length > 0) {
|
|
968
1073
|
lines.push('', `❌ 失败任务: ${report.failures.join(', ')}`)
|
|
969
1074
|
}
|
|
1075
|
+
|
|
1076
|
+
// 奖励函数注册表(reward function = policy→feedback 抽象可见)
|
|
1077
|
+
// 传 llm 列出完整注册表(task-performance 需 llm 才能跑,但构造它零 LLM 调用)。
|
|
1078
|
+
const llm = ctx.engine.getLlm() ?? ctx.engine.getRegistry()
|
|
1079
|
+
const fns = listRewardFns(llm)
|
|
1080
|
+
lines.push('', '## 🎁 奖励函数注册表', '')
|
|
1081
|
+
for (const f of fns) {
|
|
1082
|
+
lines.push(`- **${f.name}** — ${f.description}`)
|
|
1083
|
+
}
|
|
1084
|
+
lines.push('', '`/crsi eval --reward <name>` 跑指定奖励函数')
|
|
1085
|
+
|
|
1086
|
+
return { content: lines.join('\n') }
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const crsiBenchCmd: CommandHandler = async (ctx, args) => {
|
|
1090
|
+
const llm = ctx.engine.getLlm() ?? ctx.engine.getRegistry()
|
|
1091
|
+
|
|
1092
|
+
const skillIdx = args.indexOf('--skill')
|
|
1093
|
+
const skillName = skillIdx >= 0 ? args[skillIdx + 1] : undefined
|
|
1094
|
+
let skill: { name: string; text: string } | undefined
|
|
1095
|
+
if (skillName) {
|
|
1096
|
+
const body = ctx.skillsLoader?.get(skillName)?.body
|
|
1097
|
+
if (!body) {
|
|
1098
|
+
return { content: `❌ 未找到 skill: ${skillName}` }
|
|
1099
|
+
}
|
|
1100
|
+
skill = { name: skillName, text: body }
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const report = await runTaskPerformance(llm, skill ? { skill } : undefined)
|
|
1104
|
+
|
|
1105
|
+
const lines: string[] = [
|
|
1106
|
+
'## 🎯 CRSI 任务表现基准' + (skill ? `(skill: ${skill.name})` : ''),
|
|
1107
|
+
'',
|
|
1108
|
+
]
|
|
1109
|
+
lines.push(`得分: **${report.score}/100** (${report.passed}/${report.total})`, '')
|
|
1110
|
+
lines.push('| 任务 | 结果 |')
|
|
1111
|
+
lines.push('|------|------|')
|
|
1112
|
+
for (const r of report.results) {
|
|
1113
|
+
lines.push(
|
|
1114
|
+
`| ${r.description.slice(0, 60)} | ${r.passed ? '✅' : '❌'}${r.detail ? ` — ${r.detail.slice(0, 80)}` : ''} |`,
|
|
1115
|
+
)
|
|
1116
|
+
}
|
|
1117
|
+
if (report.failures.length > 0) {
|
|
1118
|
+
lines.push('', `❌ 失败任务: ${report.failures.join(', ')}`)
|
|
1119
|
+
}
|
|
970
1120
|
return { content: lines.join('\n') }
|
|
971
1121
|
}
|
|
972
1122
|
|
|
@@ -3709,6 +3859,7 @@ const upgradeCmd: CommandHandler = async (ctx) => {
|
|
|
3709
3859
|
if (ok) {
|
|
3710
3860
|
const configPath = getConfigPath()
|
|
3711
3861
|
const { existsSync } = await import('node:fs')
|
|
3862
|
+
ctx.setUpdateStatus({ state: 'installed', latest: update.latest })
|
|
3712
3863
|
lines.push('')
|
|
3713
3864
|
lines.push(t('commands.upgrade.updated', { version: update.latest }))
|
|
3714
3865
|
|
|
@@ -4695,6 +4846,7 @@ const commandsListCmd: CommandHandler = () => {
|
|
|
4695
4846
|
'/crsi propose': 'Tools & Skills',
|
|
4696
4847
|
'/crsi prose-clear': 'Tools & Skills',
|
|
4697
4848
|
'/crsi eval': 'Tools & Skills',
|
|
4849
|
+
'/crsi bench': 'Tools & Skills',
|
|
4698
4850
|
'/crsi meta': 'Tools & Skills',
|
|
4699
4851
|
'/crsi interpret': 'Tools & Skills',
|
|
4700
4852
|
'/crsi critique': 'Tools & Skills',
|
|
@@ -4853,6 +5005,7 @@ registry.set('/crsi modify', crsiModifyCmd)
|
|
|
4853
5005
|
registry.set('/crsi propose', crsiProposeCmd)
|
|
4854
5006
|
registry.set('/crsi prose-clear', crsiProseClearCmd)
|
|
4855
5007
|
registry.set('/crsi eval', crsiEvalCmd)
|
|
5008
|
+
registry.set('/crsi bench', crsiBenchCmd)
|
|
4856
5009
|
registry.set('/crsi meta', crsiMetaCmd)
|
|
4857
5010
|
registry.set('/crsi interpret', crsiInterpretCmd)
|
|
4858
5011
|
registry.set('/crsi critique', crsiCritiqueCmd)
|
|
@@ -5037,6 +5190,7 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
5037
5190
|
'固化 CRSI 失败信号(默认教训 / --rule 受管理规则 / --prose 改 skill 散文),沙箱 + 人批准门控',
|
|
5038
5191
|
'/crsi prose-clear': '清空散文提议去重 ledger(~/.mipham/crsi/prose-proposals.jsonl)',
|
|
5039
5192
|
'/crsi eval': 'Run the ground-truth CRSI eval harness and record the score',
|
|
5193
|
+
'/crsi bench': 'Run the LLM task-performance benchmark and report the score',
|
|
5040
5194
|
'/crsi meta': 'RSI Level 3 meta-rule analysis — rules that improve the rules',
|
|
5041
5195
|
'/crsi interpret': 'Tool-call behavior dashboard — error patterns, usage, health',
|
|
5042
5196
|
'/crsi critique': 'Enable/disable RLAIF self-critique on tool calls',
|
package/src/ui/input.tsx
CHANGED
|
@@ -5,6 +5,8 @@ import { getCommandList } from './commands.js'
|
|
|
5
5
|
import { CommandPicker } from './command-picker.js'
|
|
6
6
|
import { useI18n } from '../i18n-context'
|
|
7
7
|
import { discoverSessions } from '../agent/cross-session/discovery'
|
|
8
|
+
import { requestSuggestion, shouldAutocomplete, type RecentMessage } from '../core/autocomplete'
|
|
9
|
+
import type { Llm } from '../providers/llm'
|
|
8
10
|
|
|
9
11
|
interface InputBarProps {
|
|
10
12
|
onSubmit: (input: string) => void
|
|
@@ -23,6 +25,14 @@ interface InputBarProps {
|
|
|
23
25
|
onCancel?: () => void
|
|
24
26
|
/** When false, don't auto-open the slash-command picker when typing `/`. */
|
|
25
27
|
showCommandPicker?: boolean
|
|
28
|
+
/** LLM 续写建议所需的模型(app.tsx 传;RemoteEngine 下 undefined → 补全禁用)。 */
|
|
29
|
+
llm?: Llm
|
|
30
|
+
/** 最近对话上下文(供续写贴合)。 */
|
|
31
|
+
recentMessages?: RecentMessage[]
|
|
32
|
+
/** 默认 true;app.tsx 传 config.autocomplete?.enabled ?? true。 */
|
|
33
|
+
autocompleteEnabled?: boolean
|
|
34
|
+
/** 默认 400ms;app.tsx 传 config.autocomplete?.debounceMs ?? 400。 */
|
|
35
|
+
autocompleteDebounceMs?: number
|
|
26
36
|
}
|
|
27
37
|
|
|
28
38
|
// ── Loading verb keys (i18n) ──
|
|
@@ -92,6 +102,10 @@ export function InputBar({
|
|
|
92
102
|
onCyclePermission,
|
|
93
103
|
onCancel,
|
|
94
104
|
showCommandPicker = true,
|
|
105
|
+
llm,
|
|
106
|
+
recentMessages,
|
|
107
|
+
autocompleteEnabled = true,
|
|
108
|
+
autocompleteDebounceMs = 400,
|
|
95
109
|
}: InputBarProps) {
|
|
96
110
|
const { t } = useI18n()
|
|
97
111
|
const [value, setValue] = useState('')
|
|
@@ -109,6 +123,22 @@ export function InputBar({
|
|
|
109
123
|
const historyIndexRef = useRef(-1) // -1 = not browsing history
|
|
110
124
|
const savedDraftRef = useRef('') // saved user draft before browsing history
|
|
111
125
|
|
|
126
|
+
// ── Ghost-text 自动补全 ──
|
|
127
|
+
const [suggestion, setSuggestion] = useState<string | null>(null)
|
|
128
|
+
const suggestionTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
129
|
+
const suggestionReqIdRef = useRef(0)
|
|
130
|
+
|
|
131
|
+
// 统一清理:清 suggestion + 使在途/待发请求失效 + 取消防抖定时器。
|
|
132
|
+
// Escape / handleSubmit / 翻历史三处共用,避免各自手写漏掉某一环(rule of three)。
|
|
133
|
+
const clearSuggestion = () => {
|
|
134
|
+
setSuggestion(null)
|
|
135
|
+
suggestionReqIdRef.current++
|
|
136
|
+
if (suggestionTimerRef.current) {
|
|
137
|
+
clearTimeout(suggestionTimerRef.current)
|
|
138
|
+
suggestionTimerRef.current = null
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
112
142
|
// Stabilize t ref — prevents stale closures in intervals and avoids
|
|
113
143
|
// unnecessary effect re-runs when the i18n context value object changes.
|
|
114
144
|
const tRef = useRef(t)
|
|
@@ -180,6 +210,7 @@ export function InputBar({
|
|
|
180
210
|
// Idle → clear the draft (the intuitive "cancel")
|
|
181
211
|
setValue('')
|
|
182
212
|
valueRef.current = ''
|
|
213
|
+
clearSuggestion()
|
|
183
214
|
return
|
|
184
215
|
}
|
|
185
216
|
|
|
@@ -189,6 +220,14 @@ export function InputBar({
|
|
|
189
220
|
onCyclePermission?.()
|
|
190
221
|
return
|
|
191
222
|
}
|
|
223
|
+
// Tab → 接受 ghost-text 建议(复用 Ctrl-key 的 revert 手法)
|
|
224
|
+
if (key.tab && !key.shift && suggestion) {
|
|
225
|
+
const next = valueBeforeShortcut.current + suggestion
|
|
226
|
+
setValue(next)
|
|
227
|
+
valueRef.current = next
|
|
228
|
+
setSuggestion(null)
|
|
229
|
+
return
|
|
230
|
+
}
|
|
192
231
|
// Ctrl+P → toggle model picker
|
|
193
232
|
// NOTE: Ink passes input=keypress.name (just 'p') when ctrl is true, not raw \x10.
|
|
194
233
|
// ink-text-input inserts 'p' as literal text — revert it.
|
|
@@ -218,6 +257,9 @@ export function InputBar({
|
|
|
218
257
|
|
|
219
258
|
// ── Arrow-key history navigation (Claude Code parity) ──
|
|
220
259
|
if (key.upArrow || key.downArrow) {
|
|
260
|
+
// History navigation changes value via setValue (no onChange) — clear any
|
|
261
|
+
// ghost suggestion so a stale one isn't Tab-accepted onto a recalled entry.
|
|
262
|
+
clearSuggestion()
|
|
221
263
|
// Ignore if picker is active (command picker handles its own arrows)
|
|
222
264
|
if (value.startsWith('/')) return
|
|
223
265
|
|
|
@@ -299,6 +341,7 @@ export function InputBar({
|
|
|
299
341
|
setValue('')
|
|
300
342
|
valueRef.current = ''
|
|
301
343
|
setPickerActive(false)
|
|
344
|
+
clearSuggestion()
|
|
302
345
|
}
|
|
303
346
|
|
|
304
347
|
// ── Picker mode: CommandPicker overlay ──
|
|
@@ -339,6 +382,33 @@ export function InputBar({
|
|
|
339
382
|
// Normalize newlines → spaces. ink-text-input is single-line; multi-line
|
|
340
383
|
// paste would trap arrow-key navigation on the first line.
|
|
341
384
|
const normalized = val.replace(/\n/g, ' ')
|
|
385
|
+
// ── Ghost-text 自动补全:每次输入清 suggestion + 重排防抖 ──
|
|
386
|
+
setSuggestion(null)
|
|
387
|
+
const suggestionReqId = ++suggestionReqIdRef.current
|
|
388
|
+
if (suggestionTimerRef.current) {
|
|
389
|
+
clearTimeout(suggestionTimerRef.current)
|
|
390
|
+
suggestionTimerRef.current = null
|
|
391
|
+
}
|
|
392
|
+
if (
|
|
393
|
+
llm &&
|
|
394
|
+
autocompleteEnabled &&
|
|
395
|
+
shouldAutocomplete(normalized, isLoading, pickerActive)
|
|
396
|
+
) {
|
|
397
|
+
suggestionTimerRef.current = setTimeout(() => {
|
|
398
|
+
requestSuggestion(
|
|
399
|
+
llm,
|
|
400
|
+
recentMessages ?? [],
|
|
401
|
+
normalized,
|
|
402
|
+
() => suggestionReqId !== suggestionReqIdRef.current,
|
|
403
|
+
)
|
|
404
|
+
.then((completion) => {
|
|
405
|
+
if (completion) setSuggestion(completion)
|
|
406
|
+
})
|
|
407
|
+
.catch(() => {
|
|
408
|
+
// 补全失败非关键——静默忽略
|
|
409
|
+
})
|
|
410
|
+
}, autocompleteDebounceMs)
|
|
411
|
+
}
|
|
342
412
|
// 批量输入(paste/IME 替换)节流防渲染风暴;普通单字符输入立即显示,
|
|
343
413
|
// 避免 33ms trailing 的「慢半拍」尾巴。
|
|
344
414
|
const bulk = isBulkInput(valueRef.current, normalized)
|
|
@@ -363,6 +433,7 @@ export function InputBar({
|
|
|
363
433
|
isLoading ? `${verb}...` : completionVerb ? completionVerb : t('ui.input.placeholder')
|
|
364
434
|
}
|
|
365
435
|
/>
|
|
436
|
+
{suggestion && <Text dimColor>{suggestion}</Text>}
|
|
366
437
|
</Box>
|
|
367
438
|
{/* Slash command hints — shown when typing / (only when picker is NOT active) */}
|
|
368
439
|
{slashHints.length > 0 && !pickerActive && (
|