@miphamai/cli 0.78.0 → 0.79.1
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/src/shared/package-info.ts +1 -1
- package/src/ui/app.tsx +1 -1
- package/src/ui/chat.tsx +1 -3
- package/src/ui/graft-status.tsx +4 -4
- package/src/ui/input.tsx +144 -44
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
export const PACKAGE_NAME = '@miphamai/cli' as const
|
|
10
10
|
|
|
11
11
|
/** 当前发布版本 */
|
|
12
|
-
export const PACKAGE_VERSION = '0.
|
|
12
|
+
export const PACKAGE_VERSION = '0.79.1' as const
|
|
13
13
|
|
|
14
14
|
/** npm install 全局安装命令 */
|
|
15
15
|
export const NPM_INSTALL_COMMAND = `npm install -g ${PACKAGE_NAME}` as const
|
package/src/ui/app.tsx
CHANGED
|
@@ -1244,7 +1244,7 @@ export function App({
|
|
|
1244
1244
|
)}
|
|
1245
1245
|
|
|
1246
1246
|
{/* Status line — Claude Code style */}
|
|
1247
|
-
<Box
|
|
1247
|
+
<Box flexDirection="column">
|
|
1248
1248
|
<Box flexDirection="row">
|
|
1249
1249
|
<Text color={PERMISSION_COLORS[permissionMode]}>
|
|
1250
1250
|
⏵⏵ {PERMISSION_LABELS[permissionMode]}
|
package/src/ui/chat.tsx
CHANGED
|
@@ -135,9 +135,7 @@ export function ChatPanel({ messages, focusMode }: ChatPanelProps) {
|
|
|
135
135
|
</Box>
|
|
136
136
|
<Text dimColor>{t('ui.banner.tagline')}</Text>
|
|
137
137
|
<Box marginTop={1}>
|
|
138
|
-
<Text dimColor>
|
|
139
|
-
{t('ui.banner.start_message')} <Text color="yellow">/help</Text>
|
|
140
|
-
</Text>
|
|
138
|
+
<Text dimColor>{t('ui.banner.start_message')}</Text>
|
|
141
139
|
</Box>
|
|
142
140
|
<Box marginTop={1}>
|
|
143
141
|
<Text dimColor>{t('ui.banner.controls_hint')}</Text>
|
package/src/ui/graft-status.tsx
CHANGED
|
@@ -38,16 +38,16 @@ export function GraftStatusLine({ cwd, ctxPct }: { cwd: string; ctxPct?: number
|
|
|
38
38
|
<Box flexDirection="column">
|
|
39
39
|
{stats && fresh && (
|
|
40
40
|
<Box>
|
|
41
|
-
<Text
|
|
41
|
+
<Text>◤ </Text>
|
|
42
42
|
<Text color="blue">graft</Text>
|
|
43
|
-
<Text
|
|
43
|
+
<Text>
|
|
44
44
|
{' '}
|
|
45
45
|
· {stats.nodeCount} nodes / {stats.edgeCount} edges ·{' '}
|
|
46
46
|
</Text>
|
|
47
47
|
<Text color={fresh.color}>{fresh.label}</Text>
|
|
48
48
|
{saved > 0 && (
|
|
49
49
|
<>
|
|
50
|
-
<Text
|
|
50
|
+
<Text> · </Text>
|
|
51
51
|
<Text color="blue">~{saved.toLocaleString()} tok saved</Text>
|
|
52
52
|
</>
|
|
53
53
|
)}
|
|
@@ -55,7 +55,7 @@ export function GraftStatusLine({ cwd, ctxPct }: { cwd: string; ctxPct?: number
|
|
|
55
55
|
)}
|
|
56
56
|
{bottom.length > 0 && (
|
|
57
57
|
<Box>
|
|
58
|
-
<Text
|
|
58
|
+
<Text>▸ {bottom.join(' · ')}</Text>
|
|
59
59
|
</Box>
|
|
60
60
|
)}
|
|
61
61
|
</Box>
|
package/src/ui/input.tsx
CHANGED
|
@@ -86,6 +86,108 @@ export function normalizeInput(input: string): string {
|
|
|
86
86
|
return input.replace(/[\r\n\t]+/g, ' ')
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/** Cursor-aware edit state — value plus the insertion point (0..value.length). */
|
|
90
|
+
export interface EditState {
|
|
91
|
+
value: string
|
|
92
|
+
cursor: number
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A single editing keystroke, normalized away from Ink's key object for testability. */
|
|
96
|
+
export type EditAction =
|
|
97
|
+
| { type: 'moveLeft' }
|
|
98
|
+
| { type: 'moveRight' }
|
|
99
|
+
| { type: 'backspace' }
|
|
100
|
+
| { type: 'delete' }
|
|
101
|
+
| { type: 'insert'; text: string }
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Pure cursor-editing transition. Returns a new state (or the same state when
|
|
105
|
+
* the action is a no-op). Cursor is always clamped to [0, value.length].
|
|
106
|
+
*/
|
|
107
|
+
export function applyEdit(state: EditState, action: EditAction): EditState {
|
|
108
|
+
const { value, cursor } = state
|
|
109
|
+
switch (action.type) {
|
|
110
|
+
case 'moveLeft':
|
|
111
|
+
return { value, cursor: Math.max(0, cursor - 1) }
|
|
112
|
+
case 'moveRight':
|
|
113
|
+
return { value, cursor: Math.min(value.length, cursor + 1) }
|
|
114
|
+
case 'backspace':
|
|
115
|
+
if (cursor === 0) return state
|
|
116
|
+
return {
|
|
117
|
+
value: value.slice(0, cursor - 1) + value.slice(cursor),
|
|
118
|
+
cursor: cursor - 1,
|
|
119
|
+
}
|
|
120
|
+
case 'delete':
|
|
121
|
+
if (cursor >= value.length) return state
|
|
122
|
+
return { value: value.slice(0, cursor) + value.slice(cursor + 1), cursor }
|
|
123
|
+
case 'insert': {
|
|
124
|
+
if (!action.text) return state
|
|
125
|
+
return {
|
|
126
|
+
value: value.slice(0, cursor) + action.text + value.slice(cursor),
|
|
127
|
+
cursor: cursor + action.text.length,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Map an Ink key object + raw input to a cursor-editing action (null when the key
|
|
135
|
+
* isn't an edit). Extracted from the MiphamTextInput useInput handler so the macOS
|
|
136
|
+
* Backspace quirk is unit-testable.
|
|
137
|
+
*
|
|
138
|
+
* Ink 5.2.1 parses the macOS Backspace key (terminal sends \x7f) as `key.delete`,
|
|
139
|
+
* NOT `key.backspace` (that's \x08). So both must map to a backward delete; the
|
|
140
|
+
* true forward-Delete key (\x1b[3~) is also parsed as `key.delete` by Ink and is
|
|
141
|
+
* rare, so it deliberately stays backward-delete too.
|
|
142
|
+
*/
|
|
143
|
+
export function keyToEditAction(
|
|
144
|
+
key: { leftArrow?: boolean; rightArrow?: boolean; backspace?: boolean; delete?: boolean },
|
|
145
|
+
input: string,
|
|
146
|
+
): EditAction | null {
|
|
147
|
+
if (key.leftArrow) return { type: 'moveLeft' }
|
|
148
|
+
if (key.rightArrow) return { type: 'moveRight' }
|
|
149
|
+
if (key.backspace || key.delete) return { type: 'backspace' }
|
|
150
|
+
const cleaned = normalizeInput(input)
|
|
151
|
+
if (cleaned) return { type: 'insert', text: cleaned }
|
|
152
|
+
return null
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Browsing state for arrow-key history navigation. */
|
|
156
|
+
export interface HistoryNavState {
|
|
157
|
+
history: string[]
|
|
158
|
+
index: number // -1 = not browsing
|
|
159
|
+
savedDraft: string // draft saved on first up-arrow
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Pure history-navigation transition. Returns the value to display plus the
|
|
164
|
+
* updated browsing state, or null when the key is a no-op (empty history for up,
|
|
165
|
+
* or not browsing for down).
|
|
166
|
+
*/
|
|
167
|
+
export function navigateHistory(
|
|
168
|
+
state: HistoryNavState,
|
|
169
|
+
dir: 'up' | 'down',
|
|
170
|
+
draft: string,
|
|
171
|
+
): { index: number; savedDraft: string; value: string } | null {
|
|
172
|
+
if (dir === 'up') {
|
|
173
|
+
if (state.history.length === 0) return null
|
|
174
|
+
const savedDraft = state.index === -1 ? draft : state.savedDraft
|
|
175
|
+
const index = Math.min(state.index + 1, state.history.length - 1)
|
|
176
|
+
return { index, savedDraft, value: state.history[state.history.length - 1 - index]! }
|
|
177
|
+
}
|
|
178
|
+
// down
|
|
179
|
+
if (state.index === -1) return null
|
|
180
|
+
const index = state.index - 1
|
|
181
|
+
if (index === -1) {
|
|
182
|
+
return { index, savedDraft: '', value: state.savedDraft }
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
index,
|
|
186
|
+
savedDraft: state.savedDraft,
|
|
187
|
+
value: state.history[state.history.length - 1 - index]!,
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
89
191
|
/** True when typing a leading `/` should auto-open the slash-command picker. */
|
|
90
192
|
export function shouldAutoOpenPicker(value: string, prevValue: string, enabled: boolean): boolean {
|
|
91
193
|
return enabled && value.startsWith('/') && !prevValue.startsWith('/')
|
|
@@ -99,8 +201,8 @@ export function shouldAutoOpenPicker(value: string, prevValue: string, enabled:
|
|
|
99
201
|
* 边界拆成多块,同一轮 synchronous flush 里后续块读到的仍是旧值,于是「覆盖
|
|
100
202
|
* 前块 / 插到中段」,表现为粘贴内容乱序、丢失、冻住。
|
|
101
203
|
*
|
|
102
|
-
* 这里用 ref 做同步真值:每块按当前 ref
|
|
103
|
-
* React 渲染时序,分块粘贴自然累积成完整文本。
|
|
204
|
+
* 这里用 ref 做同步真值:每块按当前 ref 在光标处原子插入(默认光标在末尾),
|
|
205
|
+
* 不依赖 React 渲染时序,分块粘贴自然累积成完整文本。
|
|
104
206
|
*/
|
|
105
207
|
function MiphamTextInput({
|
|
106
208
|
value,
|
|
@@ -117,10 +219,19 @@ function MiphamTextInput({
|
|
|
117
219
|
}) {
|
|
118
220
|
// 同步真值:valueRef 永远是最新文本;受控 value 仅在渲染时落后于 ref。
|
|
119
221
|
const valueRef = useRef(value)
|
|
222
|
+
// 光标插入点(0..value.length):cursorRef 供 useInput 同步读,cursor state 驱动渲染。
|
|
223
|
+
const [cursor, setCursor] = useState(value.length)
|
|
224
|
+
const cursorRef = useRef(cursor)
|
|
120
225
|
|
|
121
|
-
//
|
|
226
|
+
// 外部改动(箭头历史回填、提交/Esc 清空)时把真值对齐回受控 prop,光标归位到末尾。
|
|
227
|
+
// 组件自身 edit 在 onChange 前已同步更新 valueRef,故 ref 与 prop 相等时跳过,
|
|
228
|
+
// 避免把「中段插入」后的光标错误拉回末尾。
|
|
122
229
|
useEffect(() => {
|
|
123
|
-
valueRef.current
|
|
230
|
+
if (valueRef.current !== value) {
|
|
231
|
+
valueRef.current = value
|
|
232
|
+
cursorRef.current = value.length
|
|
233
|
+
setCursor(value.length)
|
|
234
|
+
}
|
|
124
235
|
}, [value])
|
|
125
236
|
|
|
126
237
|
useInput(
|
|
@@ -133,21 +244,20 @@ function MiphamTextInput({
|
|
|
133
244
|
onSubmit(valueRef.current)
|
|
134
245
|
return
|
|
135
246
|
}
|
|
136
|
-
if (key.backspace || key.delete) {
|
|
137
|
-
if (valueRef.current.length > 0) {
|
|
138
|
-
const next = valueRef.current.slice(0, -1)
|
|
139
|
-
valueRef.current = next
|
|
140
|
-
onChange(next)
|
|
141
|
-
}
|
|
142
|
-
return
|
|
143
|
-
}
|
|
144
247
|
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
248
|
+
// 把按键归一化为一次光标编辑:左/右移动,退格/删除,或光标处插入(打字/粘贴)。
|
|
249
|
+
// macOS Backspace 发 \x7f,被 Ink 映射成 key.delete(非 key.backspace),
|
|
250
|
+
// 故 keyToEditAction 里两者都按向后删处理。
|
|
251
|
+
const state: EditState = { value: valueRef.current, cursor: cursorRef.current }
|
|
252
|
+
const action = keyToEditAction(key, input)
|
|
253
|
+
if (!action) return
|
|
254
|
+
|
|
255
|
+
const next = applyEdit(state, action)
|
|
256
|
+
valueRef.current = next.value
|
|
257
|
+
cursorRef.current = next.cursor
|
|
258
|
+
setCursor(next.cursor)
|
|
259
|
+
// 纯移动不通知父组件(文本未变),只在文本变化时 onChange。
|
|
260
|
+
if (next.value !== state.value) onChange(next.value)
|
|
151
261
|
},
|
|
152
262
|
{ isActive: focus },
|
|
153
263
|
)
|
|
@@ -158,8 +268,9 @@ function MiphamTextInput({
|
|
|
158
268
|
<Text dimColor>{placeholder}</Text>
|
|
159
269
|
) : (
|
|
160
270
|
<>
|
|
161
|
-
{value}
|
|
162
|
-
<Text inverse> </Text>
|
|
271
|
+
{value.slice(0, cursor)}
|
|
272
|
+
<Text inverse>{value[cursor] ?? ' '}</Text>
|
|
273
|
+
{value.slice(cursor + 1)}
|
|
163
274
|
</>
|
|
164
275
|
)}
|
|
165
276
|
</Text>
|
|
@@ -320,30 +431,19 @@ export function InputBar({
|
|
|
320
431
|
// Ignore if picker is active (command picker handles its own arrows)
|
|
321
432
|
if (value.startsWith('/')) return
|
|
322
433
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
if (historyIndexRef.current === -1) return
|
|
337
|
-
const newIndex = historyIndexRef.current - 1
|
|
338
|
-
historyIndexRef.current = newIndex
|
|
339
|
-
if (newIndex === -1) {
|
|
340
|
-
// Back to the original draft
|
|
341
|
-
setValue(savedDraftRef.current)
|
|
342
|
-
savedDraftRef.current = ''
|
|
343
|
-
} else {
|
|
344
|
-
setValue(submittedHistory[submittedHistory.length - 1 - newIndex]!)
|
|
345
|
-
}
|
|
346
|
-
return
|
|
434
|
+
const result = navigateHistory(
|
|
435
|
+
{
|
|
436
|
+
history: submittedHistory,
|
|
437
|
+
index: historyIndexRef.current,
|
|
438
|
+
savedDraft: savedDraftRef.current,
|
|
439
|
+
},
|
|
440
|
+
key.upArrow ? 'up' : 'down',
|
|
441
|
+
valueRef.current,
|
|
442
|
+
)
|
|
443
|
+
if (result) {
|
|
444
|
+
historyIndexRef.current = result.index
|
|
445
|
+
savedDraftRef.current = result.savedDraft
|
|
446
|
+
setValue(result.value)
|
|
347
447
|
}
|
|
348
448
|
}
|
|
349
449
|
})
|