@miphamai/cli 0.16.1 → 0.16.2

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.16.1",
3
+ "version": "0.16.2",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -42,6 +42,7 @@
42
42
  "test": "vitest run"
43
43
  },
44
44
  "dependencies": {
45
+ "commander": "^13.1.0",
45
46
  "ink": "^5.2.1",
46
47
  "ink-text-input": "^6.0.0",
47
48
  "react": "^18.3.1",
package/src/ui/app.tsx CHANGED
@@ -68,12 +68,12 @@ const PERMISSION_MODES: PermissionMode[] = [
68
68
  'bypassPermissions',
69
69
  ]
70
70
  const PERMISSION_LABELS: Record<PermissionMode, string> = {
71
- default: 'default · reads free',
72
- acceptEdits: 'accept edits · reads + edits free',
71
+ default: 'reads free',
72
+ acceptEdits: 'accept edits on',
73
73
  plan: 'plan mode · read-only',
74
- auto: 'auto mode · hooks gate',
75
- dontAsk: "don't ask · pre-approved only",
76
- bypassPermissions: 'bypass · skip all checks',
74
+ auto: 'auto mode',
75
+ dontAsk: "don't ask",
76
+ bypassPermissions: 'bypass',
77
77
  }
78
78
  const PERMISSION_COLORS: Record<PermissionMode, string> = {
79
79
  default: 'white',
@@ -500,7 +500,8 @@ export function App({
500
500
  )
501
501
 
502
502
  useInput((_input, key) => {
503
- // Global hotkeys
503
+ // Escape: close picker → abort loading (does NOT exit app)
504
+ // Vim mode toggle is handled by InputBar
504
505
  if (key.escape) {
505
506
  if (pickerOpen) {
506
507
  setPickerOpen(false)
@@ -510,57 +511,12 @@ export function App({
510
511
  abortRef.current.abort()
511
512
  return
512
513
  }
513
- process.exit(0)
514
- }
515
- // Ctrl+P → open model picker
516
- if (_input === '\x10') {
517
- setPickerOpen((prev) => !prev)
518
- return
519
- }
520
- // Ctrl+F → toggle focus mode
521
- if (_input === '\x06') {
522
- setFocusMode((prev) => !prev)
523
- return
524
- }
525
- // Ctrl+O → toggle last tool call expand/collapse
526
- if (_input === '\x0f') {
527
- setMessages((prev) => {
528
- const msgs = [...prev]
529
- for (let i = msgs.length - 1; i >= 0; i--) {
530
- if (msgs[i]?.toolMeta) {
531
- const meta = msgs[i]!.toolMeta!
532
- if (meta.collapsed) {
533
- msgs[i] = {
534
- ...msgs[i]!,
535
- content: `🔧 ${meta.name}: ${meta.input}\n📋 Result: ${meta.output || '(pending)'}`,
536
- toolMeta: { ...meta, collapsed: false },
537
- }
538
- } else {
539
- const short = meta.input.length > 50 ? meta.input.slice(0, 50) + '...' : meta.input
540
- msgs[i] = {
541
- ...msgs[i]!,
542
- content: `⏺ ${meta.name} · ${short} (Ctrl+O to expand)`,
543
- toolMeta: { ...meta, collapsed: true },
544
- }
545
- }
546
- break
547
- }
548
- }
549
- return msgs
550
- })
551
- return
552
- }
553
- // Shift+Tab → cycle through all 6 permission modes
554
- if (key.shift && key.tab) {
555
- setPermissionMode((prev) => {
556
- const idx = PERMISSION_MODES.indexOf(prev)
557
- const next = PERMISSION_MODES[(idx + 1) % PERMISSION_MODES.length]!
558
- // Sync to engine — use setMode for proper mode semantics
559
- engine.getPermission().setMode(next)
560
- return next
561
- })
514
+ // When input is empty and not loading, let InputBar handle Escape
515
+ // (it toggles vim mode or clears pending sequences)
562
516
  return
563
517
  }
518
+ // All other global hotkeys (Shift+Tab, Ctrl+P, Ctrl+F, Ctrl+O)
519
+ // are handled in InputBar to avoid ink-text-input conflicts
564
520
  })
565
521
 
566
522
  return (
@@ -599,7 +555,52 @@ export function App({
599
555
  /* Input bar (hidden when picker is open) */
600
556
  <Box flexDirection="column">
601
557
  <Text dimColor>──────────────────────────────</Text>
602
- <InputBar onSubmit={handleSubmit} isLoading={isLoading} />
558
+ <InputBar
559
+ onSubmit={handleSubmit}
560
+ isLoading={isLoading}
561
+ onTogglePicker={() => setPickerOpen((prev) => !prev)}
562
+ onToggleFocus={() => setFocusMode((prev) => !prev)}
563
+ onToggleExpand={() => {
564
+ setMessages((prev) => {
565
+ const msgs = [...prev]
566
+ for (let i = msgs.length - 1; i >= 0; i--) {
567
+ if (msgs[i]?.toolMeta) {
568
+ const meta = msgs[i]!.toolMeta!
569
+ if (meta.collapsed) {
570
+ msgs[i] = {
571
+ ...msgs[i]!,
572
+ content: `🔧 ${meta.name}: ${meta.input}\n📋 Result: ${meta.output || '(pending)'}`,
573
+ toolMeta: { ...meta, collapsed: false },
574
+ }
575
+ } else {
576
+ const short =
577
+ meta.input.length > 50 ? meta.input.slice(0, 50) + '...' : meta.input
578
+ msgs[i] = {
579
+ ...msgs[i]!,
580
+ content: `⏺ ${meta.name} · ${short} (Ctrl+O to expand)`,
581
+ toolMeta: { ...meta, collapsed: true },
582
+ }
583
+ }
584
+ break
585
+ }
586
+ }
587
+ return msgs
588
+ })
589
+ }}
590
+ onCyclePermission={() => {
591
+ setPermissionMode((prev) => {
592
+ const idx = PERMISSION_MODES.indexOf(prev)
593
+ const next = PERMISSION_MODES[(idx + 1) % PERMISSION_MODES.length]!
594
+ engine.getPermission().setMode(next)
595
+ return next
596
+ })
597
+ }}
598
+ onCancel={() => {
599
+ if (abortRef.current) {
600
+ abortRef.current.abort()
601
+ }
602
+ }}
603
+ />
603
604
  <Text dimColor>──────────────────────────────</Text>
604
605
  </Box>
605
606
  )}
@@ -607,7 +608,7 @@ export function App({
607
608
  {/* Agent status footer — shows running background agents */}
608
609
  <AgentFooter agents={Object.values(runningAgents)} gitBranch={gitBranch} tick={agentTick} />
609
610
 
610
- {/* Status line */}
611
+ {/* Status line — Claude Code style: compact, action-oriented */}
611
612
  <Box marginTop={1} flexDirection="column">
612
613
  {goalText && (
613
614
  <Box>
@@ -616,11 +617,10 @@ export function App({
616
617
  )}
617
618
  <Box flexDirection="row">
618
619
  <Text color={PERMISSION_COLORS[permissionMode]}>
619
- ● {PERMISSION_LABELS[permissionMode]}
620
+ ⏵⏵ {PERMISSION_LABELS[permissionMode]} (shift+tab to cycle)
620
621
  </Text>
621
- <Text dimColor> · Shift+Tab mode</Text>
622
- <Text dimColor> · Ctrl+P model · Ctrl+F focus · Ctrl+O expand</Text>
623
- <Text dimColor> · Esc cancel · /help · ← agents</Text>
622
+ <Text dimColor> · Esc to interrupt</Text>
623
+ <Text dimColor> · ← agents</Text>
624
624
  </Box>
625
625
  </Box>
626
626
  </Box>
package/src/ui/input.tsx CHANGED
@@ -8,6 +8,16 @@ import { CommandPicker } from './command-picker.js'
8
8
  interface InputBarProps {
9
9
  onSubmit: (input: string) => void
10
10
  isLoading: boolean
11
+ /** Ctrl+P → open model picker */
12
+ onTogglePicker?: () => void
13
+ /** Ctrl+F → toggle focus mode */
14
+ onToggleFocus?: () => void
15
+ /** Ctrl+O → expand last tool call */
16
+ onToggleExpand?: () => void
17
+ /** Shift+Tab → cycle permission mode */
18
+ onCyclePermission?: () => void
19
+ /** Escape → cancel loading (when input is empty) */
20
+ onCancel?: () => void
11
21
  }
12
22
 
13
23
  // ── Status verbs — English (Claude Code-aligned) + Chinese (Mipham originals) ──
@@ -107,7 +117,15 @@ function pick<T>(arr: T[]): T {
107
117
  return arr[Math.floor(Math.random() * arr.length)]!
108
118
  }
109
119
 
110
- export function InputBar({ onSubmit, isLoading }: InputBarProps) {
120
+ export function InputBar({
121
+ onSubmit,
122
+ isLoading,
123
+ onTogglePicker,
124
+ onToggleFocus,
125
+ onToggleExpand,
126
+ onCyclePermission,
127
+ onCancel,
128
+ }: InputBarProps) {
111
129
  const [value, setValue] = useState('')
112
130
  const [verb, setVerb] = useState(() => pick(STATUS_GERUNDS))
113
131
  const [completionVerb, setCompletionVerb] = useState<string | null>(null)
@@ -158,10 +176,19 @@ export function InputBar({ onSubmit, isLoading }: InputBarProps) {
158
176
  const [searchMode, setSearchMode] = useState(false)
159
177
  const [searchQuery, setSearchQuery] = useState('')
160
178
 
179
+ // Track pre-shortcut value so we can revert ink-text-input's Ctrl-key insertions.
180
+ // ink-text-input inserts 'p'/'f'/'o' for Ctrl+P/F/O (Ink normalizes control chars
181
+ // to their letter names before passing to useInput).
182
+ const valueBeforeShortcut = useRef(value)
183
+ // Keep the ref in sync with state on every change that isn't a Ctrl shortcut revert.
184
+ useEffect(() => {
185
+ valueBeforeShortcut.current = value
186
+ }, [value])
187
+
161
188
  // ── Vim motions: intercept keys in normal mode ──
162
189
 
163
190
  useInput((input, key) => {
164
- // Escape toggles between insert and normal mode
191
+ // ── Escape: cancel loading → toggle vim mode ──
165
192
  if (key.escape) {
166
193
  // Cancel search mode if active
167
194
  if (searchMode) {
@@ -169,6 +196,11 @@ export function InputBar({ onSubmit, isLoading }: InputBarProps) {
169
196
  setSearchQuery('')
170
197
  return
171
198
  }
199
+ // Escape while loading → abort (priority over vim toggle)
200
+ if (isLoading) {
201
+ onCancel?.()
202
+ return
203
+ }
172
204
  // Clear any pending multi-key sequence
173
205
  if (vimPending.current) {
174
206
  vimPending.current = null
@@ -178,6 +210,33 @@ export function InputBar({ onSubmit, isLoading }: InputBarProps) {
178
210
  return
179
211
  }
180
212
 
213
+ // ── Global hotkeys (work in both insert and normal modes) ──
214
+ // Shift+Tab → cycle permission mode
215
+ if (key.shift && key.tab) {
216
+ onCyclePermission?.()
217
+ return
218
+ }
219
+ // Ctrl+P → toggle model picker
220
+ // NOTE: Ink passes input=keypress.name (just 'p') when ctrl is true, not raw \x10.
221
+ // ink-text-input inserts 'p' as literal text — revert it.
222
+ if (key.ctrl && input === 'p') {
223
+ setValue(valueBeforeShortcut.current)
224
+ onTogglePicker?.()
225
+ return
226
+ }
227
+ // Ctrl+F → toggle focus mode
228
+ if (key.ctrl && input === 'f') {
229
+ setValue(valueBeforeShortcut.current)
230
+ onToggleFocus?.()
231
+ return
232
+ }
233
+ // Ctrl+O → expand/collapse last tool call
234
+ if (key.ctrl && input === 'o') {
235
+ setValue(valueBeforeShortcut.current)
236
+ onToggleExpand?.()
237
+ return
238
+ }
239
+
181
240
  if (vimEngine.current.mode !== 'normal') return
182
241
 
183
242
  // Handle search mode — collect query characters
package/dist/mipham DELETED
Binary file