@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miphamai/cli",
3
- "version": "0.78.0",
3
+ "version": "0.79.1",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -9,7 +9,7 @@
9
9
  export const PACKAGE_NAME = '@miphamai/cli' as const
10
10
 
11
11
  /** 当前发布版本 */
12
- export const PACKAGE_VERSION = '0.78.0' as const
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 marginTop={1} flexDirection="column">
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>
@@ -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 dimColor>◤ </Text>
41
+ <Text>◤ </Text>
42
42
  <Text color="blue">graft</Text>
43
- <Text dimColor>
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 dimColor> · </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 dimColor>▸ {bottom.join(' · ')}</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
- // 外部改动(箭头历史回填、提交清空)时,把真值对齐回受控 prop。
226
+ // 外部改动(箭头历史回填、提交/Esc 清空)时把真值对齐回受控 prop,光标归位到末尾。
227
+ // 组件自身 edit 在 onChange 前已同步更新 valueRef,故 ref 与 prop 相等时跳过,
228
+ // 避免把「中段插入」后的光标错误拉回末尾。
122
229
  useEffect(() => {
123
- valueRef.current = value
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
- const cleaned = normalizeInput(input)
147
- if (!cleaned) return
148
- const next = valueRef.current + cleaned
149
- valueRef.current = next
150
- onChange(next)
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
- if (key.upArrow) {
324
- if (submittedHistory.length === 0) return
325
- // Save current draft the first time we enter history browsing
326
- if (historyIndexRef.current === -1) {
327
- // Save the latest value from the ref — state `value` may lag a render.
328
- savedDraftRef.current = valueRef.current
329
- }
330
- const newIndex = Math.min(historyIndexRef.current + 1, submittedHistory.length - 1)
331
- historyIndexRef.current = newIndex
332
- setValue(submittedHistory[submittedHistory.length - 1 - newIndex]!)
333
- return
334
- }
335
- if (key.downArrow) {
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
  })