@miphamai/cli 0.78.0 → 0.79.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miphamai/cli",
3
- "version": "0.78.0",
3
+ "version": "0.79.0",
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.0' 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,50 @@ 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
+
89
133
  /** True when typing a leading `/` should auto-open the slash-command picker. */
90
134
  export function shouldAutoOpenPicker(value: string, prevValue: string, enabled: boolean): boolean {
91
135
  return enabled && value.startsWith('/') && !prevValue.startsWith('/')
@@ -99,8 +143,8 @@ export function shouldAutoOpenPicker(value: string, prevValue: string, enabled:
99
143
  * 边界拆成多块,同一轮 synchronous flush 里后续块读到的仍是旧值,于是「覆盖
100
144
  * 前块 / 插到中段」,表现为粘贴内容乱序、丢失、冻住。
101
145
  *
102
- * 这里用 ref 做同步真值:每块按当前 ref 原子追加(光标恒在末尾),不依赖
103
- * React 渲染时序,分块粘贴自然累积成完整文本。
146
+ * 这里用 ref 做同步真值:每块按当前 ref 在光标处原子插入(默认光标在末尾),
147
+ * 不依赖 React 渲染时序,分块粘贴自然累积成完整文本。
104
148
  */
105
149
  function MiphamTextInput({
106
150
  value,
@@ -117,10 +161,19 @@ function MiphamTextInput({
117
161
  }) {
118
162
  // 同步真值:valueRef 永远是最新文本;受控 value 仅在渲染时落后于 ref。
119
163
  const valueRef = useRef(value)
164
+ // 光标插入点(0..value.length):cursorRef 供 useInput 同步读,cursor state 驱动渲染。
165
+ const [cursor, setCursor] = useState(value.length)
166
+ const cursorRef = useRef(cursor)
120
167
 
121
- // 外部改动(箭头历史回填、提交清空)时,把真值对齐回受控 prop。
168
+ // 外部改动(箭头历史回填、提交/Esc 清空)时把真值对齐回受控 prop,光标归位到末尾。
169
+ // 组件自身 edit 在 onChange 前已同步更新 valueRef,故 ref 与 prop 相等时跳过,
170
+ // 避免把「中段插入」后的光标错误拉回末尾。
122
171
  useEffect(() => {
123
- valueRef.current = value
172
+ if (valueRef.current !== value) {
173
+ valueRef.current = value
174
+ cursorRef.current = value.length
175
+ setCursor(value.length)
176
+ }
124
177
  }, [value])
125
178
 
126
179
  useInput(
@@ -133,21 +186,26 @@ function MiphamTextInput({
133
186
  onSubmit(valueRef.current)
134
187
  return
135
188
  }
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
189
 
145
- // 打字 / 粘贴:归一化控制符后原子追加(光标恒在末尾)。
146
- const cleaned = normalizeInput(input)
147
- if (!cleaned) return
148
- const next = valueRef.current + cleaned
149
- valueRef.current = next
150
- onChange(next)
190
+ // 把按键归一化为一次光标编辑:左/右移动,退格/删除,或光标处插入(打字/粘贴)。
191
+ const state: EditState = { value: valueRef.current, cursor: cursorRef.current }
192
+ let action: EditAction | null = null
193
+ if (key.leftArrow) action = { type: 'moveLeft' }
194
+ else if (key.rightArrow) action = { type: 'moveRight' }
195
+ else if (key.backspace) action = { type: 'backspace' }
196
+ else if (key.delete) action = { type: 'delete' }
197
+ else {
198
+ const cleaned = normalizeInput(input)
199
+ if (cleaned) action = { type: 'insert', text: cleaned }
200
+ }
201
+ if (!action) return
202
+
203
+ const next = applyEdit(state, action)
204
+ valueRef.current = next.value
205
+ cursorRef.current = next.cursor
206
+ setCursor(next.cursor)
207
+ // 纯移动不通知父组件(文本未变),只在文本变化时 onChange。
208
+ if (next.value !== state.value) onChange(next.value)
151
209
  },
152
210
  { isActive: focus },
153
211
  )
@@ -158,8 +216,9 @@ function MiphamTextInput({
158
216
  <Text dimColor>{placeholder}</Text>
159
217
  ) : (
160
218
  <>
161
- {value}
162
- <Text inverse> </Text>
219
+ {value.slice(0, cursor)}
220
+ <Text inverse>{value[cursor] ?? ' '}</Text>
221
+ {value.slice(cursor + 1)}
163
222
  </>
164
223
  )}
165
224
  </Text>